puda 0.0.15__tar.gz

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-0.0.15/PKG-INFO ADDED
@@ -0,0 +1,337 @@
1
+ Metadata-Version: 2.3
2
+ Name: puda
3
+ Version: 0.0.15
4
+ Summary: Official Python SDK for PUDA, the runtime environment for Physical AI
5
+ Author: zhao
6
+ Author-email: zhao <20024592+agentzhao@users.noreply.github.com>
7
+ Requires-Dist: nats-py>=2.12.0
8
+ Requires-Dist: pydantic>=2.12.5
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Puda Comms
13
+
14
+ A Python module for communication between machines and command services via NATS messaging. Provides client-side services for sending commands, machine-side clients for receiving commands, and data models for structured message exchange.
15
+
16
+ ## Overview
17
+
18
+ The `puda` module enables asynchronous, reliable communication between command services and machines using NATS (NATS JetStream for guaranteed delivery). It handles:
19
+
20
+ - **Command execution**: Send commands to machines and receive responses
21
+ - **Message routing**: Queue commands (sequential execution) and immediate commands (control operations)
22
+ - **State management**: Thread-safe execution state tracking for cancellation and locking
23
+ - **Connection management**: Automatic NATS connection handling with async context managers
24
+
25
+ ## Components
26
+
27
+ The module consists of four main components:
28
+
29
+ ### 1. Models (`models.py`)
30
+
31
+ Data models for structured message exchange. All models use Pydantic for validation and serialization.
32
+
33
+ #### Enums
34
+
35
+ ##### `CommandResponseStatus`
36
+ Status of a command response:
37
+ - `SUCCESS`: Command executed successfully
38
+ - `ERROR`: Command execution failed
39
+
40
+ ##### `CommandResponseCode`
41
+ Error codes for command responses:
42
+ - `COMMAND_CANCELLED`: Command was cancelled before completion
43
+ - `JSON_DECODE_ERROR`: Failed to decode JSON payload
44
+ - `EXECUTION_ERROR`: General execution error
45
+ - `EXECUTION_LOCKED`: Execution is locked (another command is running)
46
+ - `UNKNOWN_COMMAND`: Command name not recognized
47
+ - `PAUSE_ERROR`: Error occurred while pausing execution
48
+ - `RESUME_ERROR`: Error occurred while resuming execution
49
+ - `NO_EXECUTION`: No execution found
50
+ - `RUN_ID_MISMATCH`: Run ID doesn't match current execution
51
+ - `CANCEL_ERROR`: Error occurred while cancelling execution
52
+ - `MACHINE_PAUSED`: Machine is currently paused
53
+
54
+ ##### `MessageType`
55
+ Type of NATS message:
56
+ - `COMMAND`: Command message sent to machine
57
+ - `RESPONSE`: Response message from machine
58
+ - `LOG`: Log message
59
+ - `ALERT`: Alert message
60
+ - `MEDIA`: Media message
61
+
62
+ ##### `ImmediateCommand`
63
+ Command names for immediate/control commands:
64
+ - `PAUSE`: Pause the current execution
65
+ - `RESUME`: Resume a paused execution
66
+ - `CANCEL`: Cancel the current execution
67
+
68
+ #### Data Models
69
+
70
+ ##### `CommandRequest`
71
+ Represents a command to be sent to a machine.
72
+
73
+ **Fields:**
74
+ - `name` (str): The command name to execute
75
+ - `machine_id` (str): Machine ID to send the command to (required)
76
+ - `params` (Dict[str, Any]): Command parameters (default: empty dict)
77
+ - `step_number` (int): Execution step number for tracking progress
78
+ - `version` (str): Command version (default: "1.0")
79
+
80
+ **Example:**
81
+ ```python
82
+ command = CommandRequest(
83
+ name="attach_tip",
84
+ machine_id="first",
85
+ params={"deck_slot": "A3", "well_name": "G8"},
86
+ step_number=2,
87
+ version="1.0"
88
+ )
89
+ ```
90
+
91
+ ##### `CommandResponse`
92
+ Represents the result of a command execution.
93
+
94
+ **Fields:**
95
+ - `status` (CommandResponseStatus): Status of the command response (SUCCESS or ERROR)
96
+ - `completed_at` (str): ISO 8601 UTC timestamp (auto-generated)
97
+ - `code` (Optional[str]): Error code if status is ERROR
98
+ - `message` (Optional[str]): Human-readable error message
99
+
100
+ **Example:**
101
+ ```python
102
+ response = CommandResponse(
103
+ status=CommandResponseStatus.SUCCESS,
104
+ completed_at="2026-01-20T02:00:46Z"
105
+ )
106
+ ```
107
+
108
+ **Error Example:**
109
+ ```python
110
+ error_response = CommandResponse(
111
+ status=CommandResponseStatus.ERROR,
112
+ code="EXECUTION_ERROR",
113
+ message="Failed to attach tip: deck_slot A3 not found",
114
+ completed_at="2026-01-20T02:00:46Z"
115
+ )
116
+ ```
117
+
118
+ ##### `MessageHeader`
119
+ Header metadata for NATS messages.
120
+
121
+ **Fields:**
122
+ - `message_type` (MessageType): Type of message (COMMAND, RESPONSE, LOG, etc.)
123
+ - `version` (str): Message version (default: "1.0")
124
+ - `timestamp` (str): ISO 8601 UTC timestamp (auto-generated)
125
+ - `user_id` (str): User ID who initiated the command
126
+ - `username` (str): Username who initiated the command
127
+ - `machine_id` (str): Identifier for the target machine
128
+ - `run_id` (Optional[str]): Unique identifier (UUID) for the run/workflow
129
+
130
+ **Example:**
131
+ ```python
132
+ header = MessageHeader(
133
+ message_type=MessageType.RESPONSE,
134
+ version="1.0",
135
+ timestamp="2026-01-20T02:00:46Z",
136
+ user_id="user123",
137
+ username="John Doe",
138
+ machine_id="first",
139
+ run_id="092073e6-13d0-4756-8d99-eff1612a5a72"
140
+ )
141
+ ```
142
+
143
+ ##### `NATSMessage`
144
+ Complete NATS message structure combining header with optional command or response data.
145
+
146
+ **Fields:**
147
+ - `header` (MessageHeader): Message header (required)
148
+ - `command` (Optional[CommandRequest]): Command request (for command messages)
149
+ - `response` (Optional[CommandResponse]): Command response (for response messages)
150
+
151
+ **Structure:**
152
+ - For command messages: include `header` with `message_type=COMMAND` and `command` field
153
+ - For response messages: include `header` with `message_type=RESPONSE` and `response` field
154
+
155
+ **Complete Message Example:**
156
+ ```json
157
+ {
158
+ "header": {
159
+ "message_type": "response",
160
+ "version": "1.0",
161
+ "timestamp": "2026-01-20T02:00:46Z",
162
+ "user_id": "user123",
163
+ "username": "John Doe",
164
+ "machine_id": "first",
165
+ "run_id": "092073e6-13d0-4756-8d99-eff1612a5a72"
166
+ },
167
+ "command": {
168
+ "name": "attach_tip",
169
+ "params": {
170
+ "deck_slot": "A3",
171
+ "well_name": "G8"
172
+ },
173
+ "step_number": 2,
174
+ "version": "1.0"
175
+ },
176
+ "response": {
177
+ "status": "success",
178
+ "completed_at": "2026-01-20T02:00:46Z",
179
+ "code": null,
180
+ "message": null
181
+ }
182
+ }
183
+ ```
184
+
185
+ ### 2. CommandService (`command_service.py`)
186
+
187
+ Client-side service for sending commands to machines via NATS. Handles:
188
+ - Connecting to NATS servers
189
+ - Sending commands to machines (queue or immediate)
190
+ - Waiting for and handling responses
191
+ - Managing command lifecycle (run_id, step_number, etc.)
192
+ - Automatic connection cleanup via async context manager
193
+
194
+ See [Sending Commands](#sending-commands) section for usage examples.
195
+
196
+ ### 3. EdgeNatsClient (`machine_client.py`)
197
+
198
+ Basic default NATS client for generic machines. Handles commands, telemetry, and events following the `puda.{machine_id}.{category}.{sub_category}` pattern. Provides:
199
+ - Subscribing to command streams (queue and immediate) via JetStream with exactly-once delivery
200
+ - Processing incoming commands and sending command responses
201
+ - Publishing telemetry (core NATS, no JetStream)
202
+ - Publishing events (core NATS, fire-and-forget)
203
+ - Connection management and reconnection handling
204
+
205
+ **Note:** This is a generic client. Machine-specific methods should be implemented in the machine-edge client.
206
+
207
+ ### 4. ExecutionState (`execution_state.py`)
208
+
209
+ Thread-safe state management for command execution. Provides:
210
+ - Execution lock to prevent concurrent commands
211
+ - Current task tracking for cancellation
212
+ - Run ID matching for cancel operations
213
+ - Thread-safe access to execution state
214
+
215
+ ## Sending Commands
216
+
217
+ The `CommandService` provides a high-level interface for sending commands to machines via NATS. See [`tests/commands.py`](tests/commands.py) and [`tests/batch_commands.py`](tests/batch_commands.py) for complete examples.
218
+
219
+ ### Recommended Usage: Async Context Manager
220
+
221
+ The recommended way to use `CommandService` is with an async context manager, which automatically handles connection and disconnection. See [`tests/commands.py`](tests/commands.py) for complete examples.
222
+
223
+ ### Command Types
224
+
225
+ #### Queue Commands
226
+
227
+ Queue commands are regular commands that are executed in sequence. Use `send_queue_command()` for machine-specific operations.
228
+
229
+ **Note:** Available commands depend on the machine you are controlling. Different machines support different command sets (e.g., `first` machine supports commands like `load_deck`, `attach_tip`, `aspirate_from`, `dispense_to`, `drop_tip`, etc.).
230
+
231
+ Both `send_queue_command()`, `send_queue_commands()`, and `send_immediate_command()` accept an optional `timeout` parameter (default: 120 seconds):
232
+
233
+ ```python
234
+ # Single command (machine_id must be in CommandRequest)
235
+ reply = await service.send_queue_command(
236
+ request=request, # request.machine_id must be set
237
+ run_id=run_id,
238
+ user_id="user123",
239
+ username="John Doe",
240
+ timeout=60 # Wait up to 60 seconds
241
+ )
242
+
243
+ # Multiple commands (timeout applies to each command)
244
+ # Each command in the list must have machine_id set
245
+ reply = await service.send_queue_commands(
246
+ requests=commands, # Each CommandRequest must have machine_id
247
+ run_id=run_id,
248
+ user_id="user123",
249
+ username="John Doe",
250
+ timeout=60 # Wait up to 60 seconds per command
251
+ )
252
+ ```
253
+
254
+ **Examples:**
255
+
256
+ See [`tests/commands.py`](tests/commands.py) for complete examples.
257
+
258
+ #### Immediate Commands
259
+
260
+ Immediate commands are control commands that interrupt or modify execution. Use `send_immediate_command()` for:
261
+ - `pause`: Pause the current execution
262
+ - `resume`: Resume a paused execution
263
+ - `cancel`: Cancel the current execution
264
+
265
+ **Examples:**
266
+
267
+ See [`tests/commands.py`](tests/commands.py) for complete examples.
268
+
269
+
270
+
271
+ ### Sending Command Sequences
272
+
273
+ You can send multiple commands in sequence using `send_queue_commands()`, which sends commands one by one and waits for each response before sending the next. If any command fails or times out, it stops immediately and returns the error response.
274
+
275
+ **Loading Commands from JSON (Recommended for LLM-generated commands):**
276
+
277
+ When generating commands from an LLM or loading from external sources, you can store commands in a JSON file and load them. See [`tests/batch_commands.py`](tests/batch_commands.py) for a complete example.
278
+
279
+ ### Error Handling
280
+
281
+ Always check the response status and handle errors appropriately:
282
+
283
+ ```python
284
+ reply: NATSMessage = await service.send_queue_command(
285
+ request=request, # request.machine_id must be set
286
+ run_id=run_id,
287
+ user_id="user123",
288
+ username="John Doe"
289
+ )
290
+
291
+ if reply is None:
292
+ # Command timed out or failed to send
293
+ logger.error("Command failed or timed out")
294
+ elif reply.response is not None and reply.response.status == CommandResponseStatus.SUCCESS:
295
+ # Command succeeded
296
+ logger.info("Command completed successfully")
297
+ else:
298
+ # Command failed with error
299
+ logger.error("Command failed with code: %s, message: %s",
300
+ reply.response.code if reply.response else None,
301
+ reply.response.message if reply.response else None)
302
+ ```
303
+
304
+ ### Configuration
305
+
306
+ #### NATS Server Configuration
307
+
308
+ The `CommandService` requires NATS server URLs to be specified explicitly. There are no default values. You must provide servers in one of two ways:
309
+
310
+ **Option 1: Via environment variable (comma-separated string)**
311
+
312
+ Set the `NATS_SERVERS` environment variable with comma-separated server URLs:
313
+
314
+ ```bash
315
+ export NATS_SERVERS="nats://192.168.50.201:4222,nats://192.168.50.201:4223,nats://192.168.50.201:4224"
316
+ ```
317
+
318
+ Then parse it when creating a `CommandService`:
319
+ ```python
320
+ import os
321
+ nats_servers = [s.strip() for s in os.getenv("NATS_SERVERS", "").split(",") if s.strip()]
322
+ service = CommandService(servers=nats_servers)
323
+ ```
324
+
325
+ **Option 2: Directly as a list**
326
+
327
+ Specify servers directly when creating a `CommandService`:
328
+ ```python
329
+ service = CommandService(servers=["nats://192.168.50.201:4222", "nats://192.168.50.201:4223", "nats://192.168.50.201:4224"])
330
+ ```
331
+ ## Validation
332
+
333
+ All models use Pydantic for validation, ensuring:
334
+ - Type checking for all fields
335
+ - Required fields are present
336
+ - Default values are applied correctly
337
+ - JSON serialization/deserialization works correctly
puda-0.0.15/README.md ADDED
@@ -0,0 +1,326 @@
1
+ # Puda Comms
2
+
3
+ A Python module for communication between machines and command services via NATS messaging. Provides client-side services for sending commands, machine-side clients for receiving commands, and data models for structured message exchange.
4
+
5
+ ## Overview
6
+
7
+ The `puda` module enables asynchronous, reliable communication between command services and machines using NATS (NATS JetStream for guaranteed delivery). It handles:
8
+
9
+ - **Command execution**: Send commands to machines and receive responses
10
+ - **Message routing**: Queue commands (sequential execution) and immediate commands (control operations)
11
+ - **State management**: Thread-safe execution state tracking for cancellation and locking
12
+ - **Connection management**: Automatic NATS connection handling with async context managers
13
+
14
+ ## Components
15
+
16
+ The module consists of four main components:
17
+
18
+ ### 1. Models (`models.py`)
19
+
20
+ Data models for structured message exchange. All models use Pydantic for validation and serialization.
21
+
22
+ #### Enums
23
+
24
+ ##### `CommandResponseStatus`
25
+ Status of a command response:
26
+ - `SUCCESS`: Command executed successfully
27
+ - `ERROR`: Command execution failed
28
+
29
+ ##### `CommandResponseCode`
30
+ Error codes for command responses:
31
+ - `COMMAND_CANCELLED`: Command was cancelled before completion
32
+ - `JSON_DECODE_ERROR`: Failed to decode JSON payload
33
+ - `EXECUTION_ERROR`: General execution error
34
+ - `EXECUTION_LOCKED`: Execution is locked (another command is running)
35
+ - `UNKNOWN_COMMAND`: Command name not recognized
36
+ - `PAUSE_ERROR`: Error occurred while pausing execution
37
+ - `RESUME_ERROR`: Error occurred while resuming execution
38
+ - `NO_EXECUTION`: No execution found
39
+ - `RUN_ID_MISMATCH`: Run ID doesn't match current execution
40
+ - `CANCEL_ERROR`: Error occurred while cancelling execution
41
+ - `MACHINE_PAUSED`: Machine is currently paused
42
+
43
+ ##### `MessageType`
44
+ Type of NATS message:
45
+ - `COMMAND`: Command message sent to machine
46
+ - `RESPONSE`: Response message from machine
47
+ - `LOG`: Log message
48
+ - `ALERT`: Alert message
49
+ - `MEDIA`: Media message
50
+
51
+ ##### `ImmediateCommand`
52
+ Command names for immediate/control commands:
53
+ - `PAUSE`: Pause the current execution
54
+ - `RESUME`: Resume a paused execution
55
+ - `CANCEL`: Cancel the current execution
56
+
57
+ #### Data Models
58
+
59
+ ##### `CommandRequest`
60
+ Represents a command to be sent to a machine.
61
+
62
+ **Fields:**
63
+ - `name` (str): The command name to execute
64
+ - `machine_id` (str): Machine ID to send the command to (required)
65
+ - `params` (Dict[str, Any]): Command parameters (default: empty dict)
66
+ - `step_number` (int): Execution step number for tracking progress
67
+ - `version` (str): Command version (default: "1.0")
68
+
69
+ **Example:**
70
+ ```python
71
+ command = CommandRequest(
72
+ name="attach_tip",
73
+ machine_id="first",
74
+ params={"deck_slot": "A3", "well_name": "G8"},
75
+ step_number=2,
76
+ version="1.0"
77
+ )
78
+ ```
79
+
80
+ ##### `CommandResponse`
81
+ Represents the result of a command execution.
82
+
83
+ **Fields:**
84
+ - `status` (CommandResponseStatus): Status of the command response (SUCCESS or ERROR)
85
+ - `completed_at` (str): ISO 8601 UTC timestamp (auto-generated)
86
+ - `code` (Optional[str]): Error code if status is ERROR
87
+ - `message` (Optional[str]): Human-readable error message
88
+
89
+ **Example:**
90
+ ```python
91
+ response = CommandResponse(
92
+ status=CommandResponseStatus.SUCCESS,
93
+ completed_at="2026-01-20T02:00:46Z"
94
+ )
95
+ ```
96
+
97
+ **Error Example:**
98
+ ```python
99
+ error_response = CommandResponse(
100
+ status=CommandResponseStatus.ERROR,
101
+ code="EXECUTION_ERROR",
102
+ message="Failed to attach tip: deck_slot A3 not found",
103
+ completed_at="2026-01-20T02:00:46Z"
104
+ )
105
+ ```
106
+
107
+ ##### `MessageHeader`
108
+ Header metadata for NATS messages.
109
+
110
+ **Fields:**
111
+ - `message_type` (MessageType): Type of message (COMMAND, RESPONSE, LOG, etc.)
112
+ - `version` (str): Message version (default: "1.0")
113
+ - `timestamp` (str): ISO 8601 UTC timestamp (auto-generated)
114
+ - `user_id` (str): User ID who initiated the command
115
+ - `username` (str): Username who initiated the command
116
+ - `machine_id` (str): Identifier for the target machine
117
+ - `run_id` (Optional[str]): Unique identifier (UUID) for the run/workflow
118
+
119
+ **Example:**
120
+ ```python
121
+ header = MessageHeader(
122
+ message_type=MessageType.RESPONSE,
123
+ version="1.0",
124
+ timestamp="2026-01-20T02:00:46Z",
125
+ user_id="user123",
126
+ username="John Doe",
127
+ machine_id="first",
128
+ run_id="092073e6-13d0-4756-8d99-eff1612a5a72"
129
+ )
130
+ ```
131
+
132
+ ##### `NATSMessage`
133
+ Complete NATS message structure combining header with optional command or response data.
134
+
135
+ **Fields:**
136
+ - `header` (MessageHeader): Message header (required)
137
+ - `command` (Optional[CommandRequest]): Command request (for command messages)
138
+ - `response` (Optional[CommandResponse]): Command response (for response messages)
139
+
140
+ **Structure:**
141
+ - For command messages: include `header` with `message_type=COMMAND` and `command` field
142
+ - For response messages: include `header` with `message_type=RESPONSE` and `response` field
143
+
144
+ **Complete Message Example:**
145
+ ```json
146
+ {
147
+ "header": {
148
+ "message_type": "response",
149
+ "version": "1.0",
150
+ "timestamp": "2026-01-20T02:00:46Z",
151
+ "user_id": "user123",
152
+ "username": "John Doe",
153
+ "machine_id": "first",
154
+ "run_id": "092073e6-13d0-4756-8d99-eff1612a5a72"
155
+ },
156
+ "command": {
157
+ "name": "attach_tip",
158
+ "params": {
159
+ "deck_slot": "A3",
160
+ "well_name": "G8"
161
+ },
162
+ "step_number": 2,
163
+ "version": "1.0"
164
+ },
165
+ "response": {
166
+ "status": "success",
167
+ "completed_at": "2026-01-20T02:00:46Z",
168
+ "code": null,
169
+ "message": null
170
+ }
171
+ }
172
+ ```
173
+
174
+ ### 2. CommandService (`command_service.py`)
175
+
176
+ Client-side service for sending commands to machines via NATS. Handles:
177
+ - Connecting to NATS servers
178
+ - Sending commands to machines (queue or immediate)
179
+ - Waiting for and handling responses
180
+ - Managing command lifecycle (run_id, step_number, etc.)
181
+ - Automatic connection cleanup via async context manager
182
+
183
+ See [Sending Commands](#sending-commands) section for usage examples.
184
+
185
+ ### 3. EdgeNatsClient (`machine_client.py`)
186
+
187
+ Basic default NATS client for generic machines. Handles commands, telemetry, and events following the `puda.{machine_id}.{category}.{sub_category}` pattern. Provides:
188
+ - Subscribing to command streams (queue and immediate) via JetStream with exactly-once delivery
189
+ - Processing incoming commands and sending command responses
190
+ - Publishing telemetry (core NATS, no JetStream)
191
+ - Publishing events (core NATS, fire-and-forget)
192
+ - Connection management and reconnection handling
193
+
194
+ **Note:** This is a generic client. Machine-specific methods should be implemented in the machine-edge client.
195
+
196
+ ### 4. ExecutionState (`execution_state.py`)
197
+
198
+ Thread-safe state management for command execution. Provides:
199
+ - Execution lock to prevent concurrent commands
200
+ - Current task tracking for cancellation
201
+ - Run ID matching for cancel operations
202
+ - Thread-safe access to execution state
203
+
204
+ ## Sending Commands
205
+
206
+ The `CommandService` provides a high-level interface for sending commands to machines via NATS. See [`tests/commands.py`](tests/commands.py) and [`tests/batch_commands.py`](tests/batch_commands.py) for complete examples.
207
+
208
+ ### Recommended Usage: Async Context Manager
209
+
210
+ The recommended way to use `CommandService` is with an async context manager, which automatically handles connection and disconnection. See [`tests/commands.py`](tests/commands.py) for complete examples.
211
+
212
+ ### Command Types
213
+
214
+ #### Queue Commands
215
+
216
+ Queue commands are regular commands that are executed in sequence. Use `send_queue_command()` for machine-specific operations.
217
+
218
+ **Note:** Available commands depend on the machine you are controlling. Different machines support different command sets (e.g., `first` machine supports commands like `load_deck`, `attach_tip`, `aspirate_from`, `dispense_to`, `drop_tip`, etc.).
219
+
220
+ Both `send_queue_command()`, `send_queue_commands()`, and `send_immediate_command()` accept an optional `timeout` parameter (default: 120 seconds):
221
+
222
+ ```python
223
+ # Single command (machine_id must be in CommandRequest)
224
+ reply = await service.send_queue_command(
225
+ request=request, # request.machine_id must be set
226
+ run_id=run_id,
227
+ user_id="user123",
228
+ username="John Doe",
229
+ timeout=60 # Wait up to 60 seconds
230
+ )
231
+
232
+ # Multiple commands (timeout applies to each command)
233
+ # Each command in the list must have machine_id set
234
+ reply = await service.send_queue_commands(
235
+ requests=commands, # Each CommandRequest must have machine_id
236
+ run_id=run_id,
237
+ user_id="user123",
238
+ username="John Doe",
239
+ timeout=60 # Wait up to 60 seconds per command
240
+ )
241
+ ```
242
+
243
+ **Examples:**
244
+
245
+ See [`tests/commands.py`](tests/commands.py) for complete examples.
246
+
247
+ #### Immediate Commands
248
+
249
+ Immediate commands are control commands that interrupt or modify execution. Use `send_immediate_command()` for:
250
+ - `pause`: Pause the current execution
251
+ - `resume`: Resume a paused execution
252
+ - `cancel`: Cancel the current execution
253
+
254
+ **Examples:**
255
+
256
+ See [`tests/commands.py`](tests/commands.py) for complete examples.
257
+
258
+
259
+
260
+ ### Sending Command Sequences
261
+
262
+ You can send multiple commands in sequence using `send_queue_commands()`, which sends commands one by one and waits for each response before sending the next. If any command fails or times out, it stops immediately and returns the error response.
263
+
264
+ **Loading Commands from JSON (Recommended for LLM-generated commands):**
265
+
266
+ When generating commands from an LLM or loading from external sources, you can store commands in a JSON file and load them. See [`tests/batch_commands.py`](tests/batch_commands.py) for a complete example.
267
+
268
+ ### Error Handling
269
+
270
+ Always check the response status and handle errors appropriately:
271
+
272
+ ```python
273
+ reply: NATSMessage = await service.send_queue_command(
274
+ request=request, # request.machine_id must be set
275
+ run_id=run_id,
276
+ user_id="user123",
277
+ username="John Doe"
278
+ )
279
+
280
+ if reply is None:
281
+ # Command timed out or failed to send
282
+ logger.error("Command failed or timed out")
283
+ elif reply.response is not None and reply.response.status == CommandResponseStatus.SUCCESS:
284
+ # Command succeeded
285
+ logger.info("Command completed successfully")
286
+ else:
287
+ # Command failed with error
288
+ logger.error("Command failed with code: %s, message: %s",
289
+ reply.response.code if reply.response else None,
290
+ reply.response.message if reply.response else None)
291
+ ```
292
+
293
+ ### Configuration
294
+
295
+ #### NATS Server Configuration
296
+
297
+ The `CommandService` requires NATS server URLs to be specified explicitly. There are no default values. You must provide servers in one of two ways:
298
+
299
+ **Option 1: Via environment variable (comma-separated string)**
300
+
301
+ Set the `NATS_SERVERS` environment variable with comma-separated server URLs:
302
+
303
+ ```bash
304
+ export NATS_SERVERS="nats://192.168.50.201:4222,nats://192.168.50.201:4223,nats://192.168.50.201:4224"
305
+ ```
306
+
307
+ Then parse it when creating a `CommandService`:
308
+ ```python
309
+ import os
310
+ nats_servers = [s.strip() for s in os.getenv("NATS_SERVERS", "").split(",") if s.strip()]
311
+ service = CommandService(servers=nats_servers)
312
+ ```
313
+
314
+ **Option 2: Directly as a list**
315
+
316
+ Specify servers directly when creating a `CommandService`:
317
+ ```python
318
+ service = CommandService(servers=["nats://192.168.50.201:4222", "nats://192.168.50.201:4223", "nats://192.168.50.201:4224"])
319
+ ```
320
+ ## Validation
321
+
322
+ All models use Pydantic for validation, ensuring:
323
+ - Type checking for all fields
324
+ - Required fields are present
325
+ - Default values are applied correctly
326
+ - JSON serialization/deserialization works correctly
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "puda"
3
+ version = "0.0.15"
4
+ description = "Official Python SDK for PUDA, the runtime environment for Physical AI"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "zhao", email = "20024592+agentzhao@users.noreply.github.com" }
8
+ ]
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "nats-py>=2.12.0",
12
+ "pydantic>=2.12.5",
13
+ ]
14
+
15
+ [tool.ruff]
16
+ line-length = 100
17
+
18
+ [tool.ruff.lint.mccabe]
19
+ max-complexity = 10
20
+
21
+ [tool.uv.build-backend]
22
+ module-name = "puda"
23
+
24
+ [build-system]
25
+ requires = ["uv_build>=0.9.18,<0.10.0"]
26
+ build-backend = "uv_build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "ruff>=0.14.13",
31
+ ]