puda-comms 0.0.4__py3-none-any.whl → 0.0.6__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.
- puda_comms/__init__.py +5 -1
- puda_comms/command_service.py +261 -85
- puda_comms/machine_client.py +215 -88
- puda_comms/models.py +7 -1
- puda_comms/run_manager.py +112 -0
- puda_comms/stream_subscriber.py +388 -0
- {puda_comms-0.0.4.dist-info → puda_comms-0.0.6.dist-info}/METADATA +12 -13
- puda_comms-0.0.6.dist-info/RECORD +10 -0
- puda_comms-0.0.4.dist-info/RECORD +0 -8
- {puda_comms-0.0.4.dist-info → puda_comms-0.0.6.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Reusable NATS JetStream subscriber for services that need to consume messages.
|
|
3
|
+
|
|
4
|
+
Provides a base class for subscribing to NATS streams with durable consumers,
|
|
5
|
+
automatic reconnection, and message handling callbacks.
|
|
6
|
+
|
|
7
|
+
This implements a push consumer pattern where NATS JetStream automatically
|
|
8
|
+
delivers messages to registered callbacks as they arrive, rather than requiring
|
|
9
|
+
the client to explicitly fetch/pull messages.
|
|
10
|
+
"""
|
|
11
|
+
import asyncio
|
|
12
|
+
import logging
|
|
13
|
+
from typing import Optional, Callable, Awaitable, List, Any
|
|
14
|
+
from abc import abstractmethod
|
|
15
|
+
import nats
|
|
16
|
+
from nats.js.client import JetStreamContext
|
|
17
|
+
from nats.aio.msg import Msg
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class StreamSubscriber:
|
|
23
|
+
"""
|
|
24
|
+
Base class for subscribing to NATS JetStream streams using push consumer pattern.
|
|
25
|
+
|
|
26
|
+
This class implements a push consumer where NATS JetStream automatically delivers
|
|
27
|
+
messages to registered callbacks as they arrive. The server pushes messages to
|
|
28
|
+
the client rather than requiring the client to pull/fetch them.
|
|
29
|
+
|
|
30
|
+
Handles connection management, durable subscriptions, and message routing.
|
|
31
|
+
Services can extend this class and implement message handling logic.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
class MyService(StreamSubscriber):
|
|
36
|
+
async def handle_message(self, msg: Msg, stream: str, subject: str):
|
|
37
|
+
# Process message
|
|
38
|
+
data = json.loads(msg.data.decode())
|
|
39
|
+
# ... your logic ...
|
|
40
|
+
await msg.ack()
|
|
41
|
+
|
|
42
|
+
service = MyService(servers=["nats://localhost:4222"])
|
|
43
|
+
await service.subscribe("STREAM_NAME", "puda.*.cmd.response.queue", "my_consumer")
|
|
44
|
+
await service.run()
|
|
45
|
+
```
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
servers: List[str],
|
|
51
|
+
connect_timeout: int = 10,
|
|
52
|
+
reconnect_time_wait: int = 2,
|
|
53
|
+
max_reconnect_attempts: int = -1
|
|
54
|
+
):
|
|
55
|
+
"""
|
|
56
|
+
Initialize the stream subscriber.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
servers: List of NATS server URLs (e.g., ["nats://localhost:4222"])
|
|
60
|
+
connect_timeout: Timeout for initial connection in seconds
|
|
61
|
+
reconnect_time_wait: Wait time between reconnection attempts in seconds
|
|
62
|
+
max_reconnect_attempts: Maximum reconnection attempts (-1 for unlimited)
|
|
63
|
+
"""
|
|
64
|
+
if not servers:
|
|
65
|
+
raise ValueError("servers must be a non-empty list")
|
|
66
|
+
|
|
67
|
+
self.servers = servers
|
|
68
|
+
self.connect_timeout = connect_timeout
|
|
69
|
+
self.reconnect_time_wait = reconnect_time_wait
|
|
70
|
+
self.max_reconnect_attempts = max_reconnect_attempts
|
|
71
|
+
|
|
72
|
+
self.nc: Optional[nats.NATS] = None
|
|
73
|
+
self.js: Optional[JetStreamContext] = None
|
|
74
|
+
self._subscriptions: List[Any] = []
|
|
75
|
+
self._is_connected = False
|
|
76
|
+
self._should_run = True
|
|
77
|
+
|
|
78
|
+
async def connect(self) -> bool:
|
|
79
|
+
"""
|
|
80
|
+
Connect to NATS servers.
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
True if connected successfully, False otherwise
|
|
84
|
+
"""
|
|
85
|
+
if self._is_connected:
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
self.nc = await nats.connect(
|
|
90
|
+
servers=self.servers,
|
|
91
|
+
connect_timeout=self.connect_timeout,
|
|
92
|
+
reconnect_time_wait=self.reconnect_time_wait,
|
|
93
|
+
max_reconnect_attempts=self.max_reconnect_attempts,
|
|
94
|
+
error_cb=self._error_callback,
|
|
95
|
+
disconnected_cb=self._disconnected_callback,
|
|
96
|
+
reconnected_cb=self._reconnected_callback,
|
|
97
|
+
closed_cb=self._closed_callback
|
|
98
|
+
)
|
|
99
|
+
self.js = self.nc.jetstream()
|
|
100
|
+
self._is_connected = True
|
|
101
|
+
logger.info("Connected to NATS servers: %s", self.servers)
|
|
102
|
+
return True
|
|
103
|
+
except Exception as e:
|
|
104
|
+
logger.error("Failed to connect to NATS: %s", e)
|
|
105
|
+
self._is_connected = False
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
async def disconnect(self):
|
|
109
|
+
"""Disconnect from NATS and cleanup subscriptions."""
|
|
110
|
+
self._should_run = False
|
|
111
|
+
|
|
112
|
+
# Unsubscribe from all streams
|
|
113
|
+
for sub in self._subscriptions:
|
|
114
|
+
try:
|
|
115
|
+
await sub.unsubscribe()
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.debug("Error unsubscribing: %s", e)
|
|
118
|
+
self._subscriptions.clear()
|
|
119
|
+
|
|
120
|
+
# Close NATS connection
|
|
121
|
+
if self.nc:
|
|
122
|
+
await self.nc.close()
|
|
123
|
+
self.nc = None
|
|
124
|
+
self.js = None
|
|
125
|
+
|
|
126
|
+
self._is_connected = False
|
|
127
|
+
logger.info("Disconnected from NATS")
|
|
128
|
+
|
|
129
|
+
async def subscribe(
|
|
130
|
+
self,
|
|
131
|
+
stream: str,
|
|
132
|
+
subject: str,
|
|
133
|
+
durable: Optional[str] = None,
|
|
134
|
+
callback: Optional[Callable[[Msg, str, str], Awaitable[None]]] = None
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
137
|
+
Subscribe to a NATS JetStream stream using push consumer pattern.
|
|
138
|
+
|
|
139
|
+
This creates a push subscription where NATS JetStream automatically delivers
|
|
140
|
+
messages to the callback as they arrive. Messages are pushed to the client
|
|
141
|
+
rather than requiring explicit fetch/pull operations.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
stream: Name of the JetStream stream
|
|
145
|
+
subject: Subject pattern to subscribe to (supports wildcards)
|
|
146
|
+
durable: Optional durable consumer name (for persistent subscriptions)
|
|
147
|
+
callback: Optional async callback function(msg, stream, subject) -> None
|
|
148
|
+
If not provided, calls handle_message() method
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
RuntimeError: If not connected to NATS
|
|
152
|
+
"""
|
|
153
|
+
if not self._is_connected or not self.js:
|
|
154
|
+
raise RuntimeError("Not connected to NATS. Call connect() first.")
|
|
155
|
+
|
|
156
|
+
# Use provided callback or default to handle_message method
|
|
157
|
+
if callback is None:
|
|
158
|
+
callback = self.handle_message
|
|
159
|
+
|
|
160
|
+
# Create callback wrapper
|
|
161
|
+
async def message_wrapper(msg: Msg):
|
|
162
|
+
try:
|
|
163
|
+
await callback(msg, stream, subject)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(
|
|
166
|
+
"Error in message callback for stream=%s, subject=%s: %s",
|
|
167
|
+
stream, subject, e, exc_info=True
|
|
168
|
+
)
|
|
169
|
+
# Don't ack on error - let the caller decide
|
|
170
|
+
# This allows for retry logic in the handler
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
# Subscribe with durable consumer if specified
|
|
174
|
+
if durable:
|
|
175
|
+
sub = await self.js.subscribe(
|
|
176
|
+
subject,
|
|
177
|
+
stream=stream,
|
|
178
|
+
durable=durable,
|
|
179
|
+
cb=lambda msg: asyncio.create_task(message_wrapper(msg))
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
# Ephemeral subscription
|
|
183
|
+
sub = await self.js.subscribe(
|
|
184
|
+
subject,
|
|
185
|
+
stream=stream,
|
|
186
|
+
cb=lambda msg: asyncio.create_task(message_wrapper(msg))
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
self._subscriptions.append(sub)
|
|
190
|
+
logger.info(
|
|
191
|
+
"Subscribed to stream=%s, subject=%s, durable=%s",
|
|
192
|
+
stream, subject, durable or "ephemeral"
|
|
193
|
+
)
|
|
194
|
+
except Exception as e:
|
|
195
|
+
error_msg = str(e)
|
|
196
|
+
# Handle the specific case where consumer is already bound
|
|
197
|
+
if "consumer is already bound" in error_msg.lower():
|
|
198
|
+
logger.warning(
|
|
199
|
+
"Consumer '%s' for stream '%s' is already bound. "
|
|
200
|
+
"This usually happens when the service didn't shut down cleanly. "
|
|
201
|
+
"Attempting to delete the consumer and retry...",
|
|
202
|
+
durable, stream
|
|
203
|
+
)
|
|
204
|
+
if durable:
|
|
205
|
+
try:
|
|
206
|
+
# Try to delete the consumer (may fail if actively bound)
|
|
207
|
+
await self.js.delete_consumer(stream, durable)
|
|
208
|
+
logger.info("Deleted consumer '%s' for stream '%s'", durable, stream)
|
|
209
|
+
# Retry subscription after deletion
|
|
210
|
+
sub = await self.js.subscribe(
|
|
211
|
+
subject,
|
|
212
|
+
stream=stream,
|
|
213
|
+
durable=durable,
|
|
214
|
+
cb=lambda msg: asyncio.create_task(message_wrapper(msg))
|
|
215
|
+
)
|
|
216
|
+
self._subscriptions.append(sub)
|
|
217
|
+
logger.info(
|
|
218
|
+
"Successfully subscribed after consumer cleanup: stream=%s, subject=%s, durable=%s",
|
|
219
|
+
stream, subject, durable
|
|
220
|
+
)
|
|
221
|
+
except Exception as retry_error:
|
|
222
|
+
retry_error_msg = str(retry_error)
|
|
223
|
+
if "bound" in retry_error_msg.lower() or "in use" in retry_error_msg.lower():
|
|
224
|
+
logger.error(
|
|
225
|
+
"Consumer '%s' for stream '%s' cannot be deleted because it's still bound. "
|
|
226
|
+
"This typically means the previous service instance is still running or "
|
|
227
|
+
"the subscription hasn't timed out yet. Solutions:\n"
|
|
228
|
+
" 1. Wait a few seconds and restart the service\n"
|
|
229
|
+
" 2. Manually delete the consumer: nats consumer rm %s %s\n"
|
|
230
|
+
" 3. Restart the NATS server\n"
|
|
231
|
+
" 4. Use a different durable consumer name",
|
|
232
|
+
durable, stream, stream, durable
|
|
233
|
+
)
|
|
234
|
+
else:
|
|
235
|
+
logger.error(
|
|
236
|
+
"Failed to delete consumer '%s' for stream '%s': %s",
|
|
237
|
+
durable, stream, retry_error
|
|
238
|
+
)
|
|
239
|
+
raise
|
|
240
|
+
else:
|
|
241
|
+
raise
|
|
242
|
+
else:
|
|
243
|
+
logger.error(
|
|
244
|
+
"Failed to subscribe to stream=%s, subject=%s: %s",
|
|
245
|
+
stream, subject, e
|
|
246
|
+
)
|
|
247
|
+
raise
|
|
248
|
+
|
|
249
|
+
@abstractmethod
|
|
250
|
+
async def handle_message(self, msg: Msg, stream: str, subject: str):
|
|
251
|
+
"""
|
|
252
|
+
Handle an incoming message pushed by NATS JetStream. Override this method in subclasses.
|
|
253
|
+
|
|
254
|
+
This method is called automatically when NATS JetStream pushes a message
|
|
255
|
+
to this subscriber. The push consumer pattern means messages arrive
|
|
256
|
+
asynchronously via callbacks rather than being explicitly fetched.
|
|
257
|
+
|
|
258
|
+
Default implementation logs and acks the message.
|
|
259
|
+
Subclasses should implement their own message processing logic.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
msg: NATS message object
|
|
263
|
+
stream: Name of the stream the message came from
|
|
264
|
+
subject: Subject pattern that matched this message
|
|
265
|
+
"""
|
|
266
|
+
logger.debug(
|
|
267
|
+
"Received message from stream=%s, subject=%s, data_size=%d",
|
|
268
|
+
stream, subject, len(msg.data)
|
|
269
|
+
)
|
|
270
|
+
# Default: ack the message
|
|
271
|
+
await msg.ack()
|
|
272
|
+
|
|
273
|
+
async def _error_callback(self, error: Exception):
|
|
274
|
+
"""Callback for NATS errors."""
|
|
275
|
+
if error:
|
|
276
|
+
logger.error("NATS error: %s", error, exc_info=True)
|
|
277
|
+
else:
|
|
278
|
+
logger.error("NATS error: Unknown error (error object is None)")
|
|
279
|
+
|
|
280
|
+
async def _disconnected_callback(self):
|
|
281
|
+
"""Callback when disconnected from NATS."""
|
|
282
|
+
logger.warning("Disconnected from NATS servers")
|
|
283
|
+
self._is_connected = False
|
|
284
|
+
|
|
285
|
+
async def _reconnected_callback(self):
|
|
286
|
+
"""Callback when reconnected to NATS."""
|
|
287
|
+
logger.info("Reconnected to NATS servers")
|
|
288
|
+
self._is_connected = True
|
|
289
|
+
if self.nc:
|
|
290
|
+
self.js = self.nc.jetstream()
|
|
291
|
+
# Clear old subscriptions as they're no longer valid after reconnection
|
|
292
|
+
self._subscriptions.clear()
|
|
293
|
+
# Re-subscribe to all streams
|
|
294
|
+
await self._resubscribe_all()
|
|
295
|
+
|
|
296
|
+
async def _closed_callback(self):
|
|
297
|
+
"""Callback when connection is closed."""
|
|
298
|
+
logger.info("NATS connection closed")
|
|
299
|
+
self._is_connected = False
|
|
300
|
+
|
|
301
|
+
async def _resubscribe_all(self):
|
|
302
|
+
"""
|
|
303
|
+
Re-subscribe to all streams after reconnection.
|
|
304
|
+
|
|
305
|
+
Override this method in subclasses to restore subscriptions.
|
|
306
|
+
The default implementation does nothing - subclasses should track
|
|
307
|
+
their subscriptions and re-subscribe here.
|
|
308
|
+
"""
|
|
309
|
+
logger.debug("Reconnection detected, but no subscriptions to restore")
|
|
310
|
+
|
|
311
|
+
async def run(self, health_check_interval: float = 1.0):
|
|
312
|
+
"""
|
|
313
|
+
Run the subscriber service with connection health monitoring.
|
|
314
|
+
|
|
315
|
+
This method will:
|
|
316
|
+
1. Connect to NATS (with retry logic)
|
|
317
|
+
2. Call on_start() hook for subclasses to set up subscriptions
|
|
318
|
+
3. Monitor connection health and reconnect if needed
|
|
319
|
+
4. Call on_stop() hook on shutdown
|
|
320
|
+
|
|
321
|
+
Args:
|
|
322
|
+
health_check_interval: Interval in seconds to check connection health
|
|
323
|
+
"""
|
|
324
|
+
# Connect to NATS with retry logic
|
|
325
|
+
while self._should_run:
|
|
326
|
+
if await self.connect():
|
|
327
|
+
break
|
|
328
|
+
logger.warning("Failed to connect to NATS, retrying in 5 seconds...")
|
|
329
|
+
await asyncio.sleep(5)
|
|
330
|
+
|
|
331
|
+
# Call on_start hook for subclasses to set up subscriptions
|
|
332
|
+
await self.on_start()
|
|
333
|
+
|
|
334
|
+
logger.info("Stream subscriber service started")
|
|
335
|
+
|
|
336
|
+
# Main loop with health monitoring
|
|
337
|
+
try:
|
|
338
|
+
while self._should_run:
|
|
339
|
+
await asyncio.sleep(health_check_interval)
|
|
340
|
+
|
|
341
|
+
# Check connection health
|
|
342
|
+
if not self._is_connected:
|
|
343
|
+
logger.warning("Connection lost, attempting to reconnect...")
|
|
344
|
+
if await self.connect():
|
|
345
|
+
# Clear old subscriptions as they're no longer valid after reconnection
|
|
346
|
+
self._subscriptions.clear()
|
|
347
|
+
await self._resubscribe_all()
|
|
348
|
+
except KeyboardInterrupt:
|
|
349
|
+
logger.info("Received KeyboardInterrupt, shutting down...")
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.error("Unexpected error in main loop: %s", e, exc_info=True)
|
|
352
|
+
finally:
|
|
353
|
+
await self.on_stop()
|
|
354
|
+
await self.disconnect()
|
|
355
|
+
|
|
356
|
+
@abstractmethod
|
|
357
|
+
async def on_start(self):
|
|
358
|
+
"""
|
|
359
|
+
Hook called when the service starts. Override in subclasses to set up subscriptions.
|
|
360
|
+
|
|
361
|
+
Example:
|
|
362
|
+
```python
|
|
363
|
+
async def on_start(self):
|
|
364
|
+
await self.subscribe("STREAM_NAME", "puda.*.cmd.response.queue", "my_consumer")
|
|
365
|
+
await self.subscribe("STREAM_NAME", "puda.*.cmd.response.immediate", "my_consumer2")
|
|
366
|
+
```
|
|
367
|
+
"""
|
|
368
|
+
pass
|
|
369
|
+
|
|
370
|
+
@abstractmethod
|
|
371
|
+
async def on_stop(self):
|
|
372
|
+
"""
|
|
373
|
+
Hook called when the service stops. Override in subclasses for cleanup.
|
|
374
|
+
"""
|
|
375
|
+
pass
|
|
376
|
+
|
|
377
|
+
# ==================== Context Manager ====================
|
|
378
|
+
|
|
379
|
+
async def __aenter__(self):
|
|
380
|
+
"""Async context manager entry."""
|
|
381
|
+
await self.connect()
|
|
382
|
+
return self
|
|
383
|
+
|
|
384
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
385
|
+
"""Async context manager exit."""
|
|
386
|
+
await self.disconnect()
|
|
387
|
+
return False # Don't suppress exceptions
|
|
388
|
+
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: puda-comms
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: Communication library for the PUDA platform.
|
|
5
5
|
Author: zhao
|
|
6
6
|
Author-email: zhao <20024592+agentzhao@users.noreply.github.com>
|
|
7
7
|
Requires-Dist: nats-py>=2.12.0
|
|
8
|
-
Requires-Dist: puda-drivers
|
|
9
8
|
Requires-Dist: pydantic>=2.12.5
|
|
10
9
|
Requires-Python: >=3.14
|
|
11
10
|
Description-Content-Type: text/markdown
|
|
@@ -73,6 +72,7 @@ Represents a command to be sent to a machine.
|
|
|
73
72
|
|
|
74
73
|
**Fields:**
|
|
75
74
|
- `name` (str): The command name to execute
|
|
75
|
+
- `machine_id` (str): Machine ID to send the command to (required)
|
|
76
76
|
- `params` (Dict[str, Any]): Command parameters (default: empty dict)
|
|
77
77
|
- `step_number` (int): Execution step number for tracking progress
|
|
78
78
|
- `version` (str): Command version (default: "1.0")
|
|
@@ -81,7 +81,8 @@ Represents a command to be sent to a machine.
|
|
|
81
81
|
```python
|
|
82
82
|
command = CommandRequest(
|
|
83
83
|
name="attach_tip",
|
|
84
|
-
|
|
84
|
+
machine_id="first",
|
|
85
|
+
params={"deck_slot": "A3", "well_name": "G8"},
|
|
85
86
|
step_number=2,
|
|
86
87
|
version="1.0"
|
|
87
88
|
)
|
|
@@ -109,7 +110,7 @@ response = CommandResponse(
|
|
|
109
110
|
error_response = CommandResponse(
|
|
110
111
|
status=CommandResponseStatus.ERROR,
|
|
111
112
|
code="EXECUTION_ERROR",
|
|
112
|
-
message="Failed to attach tip:
|
|
113
|
+
message="Failed to attach tip: deck_slot A3 not found",
|
|
113
114
|
completed_at="2026-01-20T02:00:46Z"
|
|
114
115
|
)
|
|
115
116
|
```
|
|
@@ -166,8 +167,8 @@ Complete NATS message structure combining header with optional command or respon
|
|
|
166
167
|
"command": {
|
|
167
168
|
"name": "attach_tip",
|
|
168
169
|
"params": {
|
|
169
|
-
"
|
|
170
|
-
"
|
|
170
|
+
"deck_slot": "A3",
|
|
171
|
+
"well_name": "G8"
|
|
171
172
|
},
|
|
172
173
|
"step_number": 2,
|
|
173
174
|
"version": "1.0"
|
|
@@ -230,10 +231,9 @@ Queue commands are regular commands that are executed in sequence. Use `send_que
|
|
|
230
231
|
Both `send_queue_command()`, `send_queue_commands()`, and `send_immediate_command()` accept an optional `timeout` parameter (default: 120 seconds):
|
|
231
232
|
|
|
232
233
|
```python
|
|
233
|
-
# Single command
|
|
234
|
+
# Single command (machine_id must be in CommandRequest)
|
|
234
235
|
reply = await service.send_queue_command(
|
|
235
|
-
request=request,
|
|
236
|
-
machine_id="first",
|
|
236
|
+
request=request, # request.machine_id must be set
|
|
237
237
|
run_id=run_id,
|
|
238
238
|
user_id="user123",
|
|
239
239
|
username="John Doe",
|
|
@@ -241,9 +241,9 @@ reply = await service.send_queue_command(
|
|
|
241
241
|
)
|
|
242
242
|
|
|
243
243
|
# Multiple commands (timeout applies to each command)
|
|
244
|
+
# Each command in the list must have machine_id set
|
|
244
245
|
reply = await service.send_queue_commands(
|
|
245
|
-
requests=commands,
|
|
246
|
-
machine_id="first",
|
|
246
|
+
requests=commands, # Each CommandRequest must have machine_id
|
|
247
247
|
run_id=run_id,
|
|
248
248
|
user_id="user123",
|
|
249
249
|
username="John Doe",
|
|
@@ -282,8 +282,7 @@ Always check the response status and handle errors appropriately:
|
|
|
282
282
|
|
|
283
283
|
```python
|
|
284
284
|
reply: NATSMessage = await service.send_queue_command(
|
|
285
|
-
request=request,
|
|
286
|
-
machine_id="first",
|
|
285
|
+
request=request, # request.machine_id must be set
|
|
287
286
|
run_id=run_id,
|
|
288
287
|
user_id="user123",
|
|
289
288
|
username="John Doe"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
puda_comms/__init__.py,sha256=2HHpV6ypjlkvrZz6OxguelX3GhqHvztjZT0b9bVSFq8,387
|
|
2
|
+
puda_comms/command_service.py,sha256=BBzYPd4DFAbxtVRgsxHkVjiip0C4PDdT125G5QIUe68,33591
|
|
3
|
+
puda_comms/execution_state.py,sha256=aTaejCnJgg1y_FP-ymIC1GQzqC81FIWo0RZ18XzAQnA,2881
|
|
4
|
+
puda_comms/machine_client.py,sha256=13WYvg49b8XgawRBiSIiCAu-XYUgIP0xzN7Ec8PRHXA,43504
|
|
5
|
+
puda_comms/models.py,sha256=yRd8RuKl4mIAfEZ2-WqsSfzPkU2ZpLoxPlEV6jeeUmU,3911
|
|
6
|
+
puda_comms/run_manager.py,sha256=_s4VYVGwtRMcduz95_DPIObso4uWRS24n5NH7AiGgjI,3591
|
|
7
|
+
puda_comms/stream_subscriber.py,sha256=coT2M_e-sNf0HeIkmsiynjPq6Rn4B-sqt1eX306O_oA,15475
|
|
8
|
+
puda_comms-0.0.6.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
9
|
+
puda_comms-0.0.6.dist-info/METADATA,sha256=f3J0A9qNPZ-cgF2NeDSUfTBYGbJx3U-m54m9PAPQLdE,11731
|
|
10
|
+
puda_comms-0.0.6.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
puda_comms/__init__.py,sha256=lntvVFJJez_rv5lZy5mYj4_43B9Y3NRNzxWfBuSAQ1M,194
|
|
2
|
-
puda_comms/command_service.py,sha256=KFremcEGfsTeUVQMIhyk1knYmUCvRYQ12vS_jy_14wA,25193
|
|
3
|
-
puda_comms/execution_state.py,sha256=aTaejCnJgg1y_FP-ymIC1GQzqC81FIWo0RZ18XzAQnA,2881
|
|
4
|
-
puda_comms/machine_client.py,sha256=wj6t_QHGs7l1Oc8JQ6hq2hqBd5C14TCPA_dTU9qOLzw,37430
|
|
5
|
-
puda_comms/models.py,sha256=9ZGX0PR7SgMBOL5zVLrPuSUhZqutQU96PubyjyQLhf8,3617
|
|
6
|
-
puda_comms-0.0.4.dist-info/WHEEL,sha256=ZyFSCYkV2BrxH6-HRVRg3R9Fo7MALzer9KiPYqNxSbo,79
|
|
7
|
-
puda_comms-0.0.4.dist-info/METADATA,sha256=0cMHDub_3NZt7Cj5U1jzrQXI8atQqpMM-i3vSMrT5lo,11512
|
|
8
|
-
puda_comms-0.0.4.dist-info/RECORD,,
|
|
File without changes
|