commandnet 0.4.2__tar.gz → 0.5.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commandnet
3
- Version: 0.4.2
3
+ Version: 0.5.0
4
4
  Summary: A lightweight, Pydantic-powered, distributed event-driven state machine and typed node graph runtime.
5
5
  Author: Christopher Vaz
6
6
  Author-email: christophervaz160@gmail.com
@@ -11,7 +11,6 @@ class Event(BaseModel):
11
11
  subject_id: str
12
12
  node_name: str
13
13
  payload: Optional[Dict[str, Any]] = None
14
- headers: Dict[str, str] = Field(default_factory=dict)
15
14
 
16
15
  timestamp: str = Field(default_factory=utcnow_iso)
17
16
  run_at: str = Field(default_factory=utcnow_iso)
@@ -127,9 +127,6 @@ class Engine:
127
127
  duration: float = 0.0,
128
128
  payload: Any = None,
129
129
  ):
130
- headers = {}
131
- if hasattr(context, "trace_headers") and context.trace_headers:
132
- headers = context.trace_headers
133
130
  # 1. Interrupt (Cancellation)
134
131
  if isinstance(target, Interrupt):
135
132
  await self.cancel_subject(target.subject_id, target.hard)
@@ -168,7 +165,6 @@ class Engine:
168
165
  key = subject_id.split("#")[1]
169
166
  waiters = await self.db.resolve_call_group(key)
170
167
  for w in waiters:
171
- # Resume waiters using the final context of the shared node as payload
172
168
  await self._apply_target(
173
169
  w["subject_id"],
174
170
  w["context"],
@@ -194,14 +190,23 @@ class Engine:
194
190
  node_name = target.get_node_name()
195
191
  await self.observer.on_transition(subject_id, "RUN", node_name, duration)
196
192
 
193
+ # PROPAGATION: Use provided payload
197
194
  p_load = payload.model_dump() if hasattr(payload, "model_dump") else payload
198
195
 
199
- evt = Event(subject_id=subject_id, node_name=node_name, payload=p_load, headers=headers)
196
+ evt = Event(
197
+ subject_id=subject_id,
198
+ node_name=node_name,
199
+ payload=p_load,
200
+ )
200
201
  ctx_dict = self._dump_ctx(context)
201
202
 
202
203
  if "#" in subject_id:
203
204
  await self.db.save_sub_state(
204
- subject_id, subject_id.split("#")[0], node_name, ctx_dict, evt
205
+ subject_id,
206
+ subject_id.split("#")[0],
207
+ node_name,
208
+ ctx_dict,
209
+ evt,
205
210
  )
206
211
  else:
207
212
  await self.db.save_state(subject_id, node_name, ctx_dict, evt)
@@ -213,6 +218,7 @@ class Engine:
213
218
  if isinstance(target, Wait):
214
219
  actual_id = subject_id
215
220
  actual_ctx = context
221
+
216
222
  if target.sub_context_path and "#" not in subject_id:
217
223
  actual_id = f"{subject_id}#{target.sub_context_path}"
218
224
  actual_ctx = self._get_path(context, target.sub_context_path)
@@ -220,6 +226,7 @@ class Engine:
220
226
  await self.observer.on_transition(
221
227
  actual_id, "RUN", f"WAIT:{target.signal_id}", duration
222
228
  )
229
+
223
230
  await self.db.park_subject(
224
231
  actual_id,
225
232
  target.signal_id,
@@ -230,7 +237,12 @@ class Engine:
230
237
 
231
238
  # 6. Parallel Fan-out
232
239
  if isinstance(target, Parallel):
233
- join_name = target.join_node.get_node_name() if target.join_node else "FORK"
240
+ join_name = (
241
+ target.join_node.get_node_name()
242
+ if target.join_node
243
+ else "FORK"
244
+ )
245
+
234
246
  await self.observer.on_transition(
235
247
  subject_id, "RUN", f"PARALLEL:{join_name}", duration
236
248
  )
@@ -241,11 +253,10 @@ class Engine:
241
253
  )
242
254
 
243
255
  for branch in target.branches:
244
- # Normalize branch to ParallelTask without stripping Wait wrappers
256
+ # Normalize branch into ParallelTask
245
257
  if isinstance(branch, ParallelTask):
246
258
  task = branch
247
259
  else:
248
- # If it's a Wait or a Node class, wrap it but keep the action intact
249
260
  path = (
250
261
  branch.sub_context_path
251
262
  if isinstance(branch, Wait) and branch.sub_context_path
@@ -253,30 +264,42 @@ class Engine:
253
264
  )
254
265
  task = ParallelTask(action=branch, sub_context_path=path)
255
266
 
267
+ # PROPAGATION: fallback to incoming payload if branch has none
268
+ branch_payload = (
269
+ task.payload if task.payload is not None else payload
270
+ )
271
+
256
272
  sub_ctx = self._get_path(context, task.sub_context_path)
273
+
257
274
  await self._apply_target(
258
275
  f"{subject_id}#{task.sub_context_path}",
259
276
  sub_ctx,
260
277
  task.action,
261
- payload=task.payload,
278
+ payload=branch_payload,
262
279
  )
263
280
 
264
281
  if target.join_node:
265
282
  await self.db.save_state(
266
- subject_id, "WAITING_FOR_JOIN", self._dump_ctx(context), None
283
+ subject_id,
284
+ "WAITING_FOR_JOIN",
285
+ self._dump_ctx(context),
286
+ None,
267
287
  )
268
288
  else:
269
289
  await self._apply_target(subject_id, context, None)
290
+
270
291
  return
271
292
 
272
293
  # 7. Schedule (Delayed Execution)
273
294
  if isinstance(target, Schedule):
274
295
  if not (
275
- isinstance(target.action, type) and issubclass(target.action, Node)
296
+ isinstance(target.action, type)
297
+ and issubclass(target.action, Node)
276
298
  ):
277
299
  raise TypeError("Schedule.action must be a Node class.")
278
300
 
279
301
  target_node_name = target.action.get_node_name()
302
+
280
303
  await self.observer.on_transition(
281
304
  subject_id, "RUN", f"SCHEDULED:{target_node_name}", duration
282
305
  )
@@ -284,10 +307,16 @@ class Engine:
284
307
  run_at_dt = datetime.now(timezone.utc) + timedelta(
285
308
  seconds=target.delay_seconds
286
309
  )
310
+
311
+ # PROPAGATION: fallback to incoming payload if schedule payload is missing
312
+ final_payload = (
313
+ target.payload if target.payload is not None else payload
314
+ )
315
+
287
316
  p_load = (
288
- target.payload.model_dump()
289
- if hasattr(target.payload, "model_dump")
290
- else target.payload
317
+ final_payload.model_dump()
318
+ if hasattr(final_payload, "model_dump")
319
+ else final_payload
291
320
  )
292
321
 
293
322
  scheduled_evt = Event(
@@ -296,15 +325,16 @@ class Engine:
296
325
  payload=p_load,
297
326
  run_at=run_at_dt.isoformat(),
298
327
  idempotency_key=target.idempotency_key,
299
- headers=headers,
300
328
  )
301
329
 
302
330
  await self.db.schedule_event(scheduled_evt)
303
331
  await self.db.save_state(
304
- subject_id, target_node_name, self._dump_ctx(context), None
332
+ subject_id,
333
+ target_node_name,
334
+ self._dump_ctx(context),
335
+ None,
305
336
  )
306
337
  return
307
-
308
338
  # --- EXTERNAL CONTROL & SIGNALS ---
309
339
 
310
340
  async def signal_node(self, subject_id: str, signal_id: str, payload: Any = None):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "commandnet"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "A lightweight, Pydantic-powered, distributed event-driven state machine and typed node graph runtime."
5
5
  authors = [
6
6
  { name = "Christopher Vaz", email = "christophervaz160@gmail.com" }
File without changes