commandnet 0.4.1__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.1
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)
@@ -165,7 +165,6 @@ class Engine:
165
165
  key = subject_id.split("#")[1]
166
166
  waiters = await self.db.resolve_call_group(key)
167
167
  for w in waiters:
168
- # Resume waiters using the final context of the shared node as payload
169
168
  await self._apply_target(
170
169
  w["subject_id"],
171
170
  w["context"],
@@ -191,18 +190,23 @@ class Engine:
191
190
  node_name = target.get_node_name()
192
191
  await self.observer.on_transition(subject_id, "RUN", node_name, duration)
193
192
 
193
+ # PROPAGATION: Use provided payload
194
194
  p_load = payload.model_dump() if hasattr(payload, "model_dump") else payload
195
195
 
196
- headers = {}
197
- if hasattr(context, "trace_headers") and context.trace_headers:
198
- headers = context.trace_headers
199
-
200
- 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
+ )
201
201
  ctx_dict = self._dump_ctx(context)
202
202
 
203
203
  if "#" in subject_id:
204
204
  await self.db.save_sub_state(
205
- 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,
206
210
  )
207
211
  else:
208
212
  await self.db.save_state(subject_id, node_name, ctx_dict, evt)
@@ -214,6 +218,7 @@ class Engine:
214
218
  if isinstance(target, Wait):
215
219
  actual_id = subject_id
216
220
  actual_ctx = context
221
+
217
222
  if target.sub_context_path and "#" not in subject_id:
218
223
  actual_id = f"{subject_id}#{target.sub_context_path}"
219
224
  actual_ctx = self._get_path(context, target.sub_context_path)
@@ -221,6 +226,7 @@ class Engine:
221
226
  await self.observer.on_transition(
222
227
  actual_id, "RUN", f"WAIT:{target.signal_id}", duration
223
228
  )
229
+
224
230
  await self.db.park_subject(
225
231
  actual_id,
226
232
  target.signal_id,
@@ -231,7 +237,12 @@ class Engine:
231
237
 
232
238
  # 6. Parallel Fan-out
233
239
  if isinstance(target, Parallel):
234
- 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
+
235
246
  await self.observer.on_transition(
236
247
  subject_id, "RUN", f"PARALLEL:{join_name}", duration
237
248
  )
@@ -242,11 +253,10 @@ class Engine:
242
253
  )
243
254
 
244
255
  for branch in target.branches:
245
- # Normalize branch to ParallelTask without stripping Wait wrappers
256
+ # Normalize branch into ParallelTask
246
257
  if isinstance(branch, ParallelTask):
247
258
  task = branch
248
259
  else:
249
- # If it's a Wait or a Node class, wrap it but keep the action intact
250
260
  path = (
251
261
  branch.sub_context_path
252
262
  if isinstance(branch, Wait) and branch.sub_context_path
@@ -254,30 +264,42 @@ class Engine:
254
264
  )
255
265
  task = ParallelTask(action=branch, sub_context_path=path)
256
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
+
257
272
  sub_ctx = self._get_path(context, task.sub_context_path)
273
+
258
274
  await self._apply_target(
259
275
  f"{subject_id}#{task.sub_context_path}",
260
276
  sub_ctx,
261
277
  task.action,
262
- payload=task.payload,
278
+ payload=branch_payload,
263
279
  )
264
280
 
265
281
  if target.join_node:
266
282
  await self.db.save_state(
267
- subject_id, "WAITING_FOR_JOIN", self._dump_ctx(context), None
283
+ subject_id,
284
+ "WAITING_FOR_JOIN",
285
+ self._dump_ctx(context),
286
+ None,
268
287
  )
269
288
  else:
270
289
  await self._apply_target(subject_id, context, None)
290
+
271
291
  return
272
292
 
273
293
  # 7. Schedule (Delayed Execution)
274
294
  if isinstance(target, Schedule):
275
295
  if not (
276
- isinstance(target.action, type) and issubclass(target.action, Node)
296
+ isinstance(target.action, type)
297
+ and issubclass(target.action, Node)
277
298
  ):
278
299
  raise TypeError("Schedule.action must be a Node class.")
279
300
 
280
301
  target_node_name = target.action.get_node_name()
302
+
281
303
  await self.observer.on_transition(
282
304
  subject_id, "RUN", f"SCHEDULED:{target_node_name}", duration
283
305
  )
@@ -285,10 +307,16 @@ class Engine:
285
307
  run_at_dt = datetime.now(timezone.utc) + timedelta(
286
308
  seconds=target.delay_seconds
287
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
+
288
316
  p_load = (
289
- target.payload.model_dump()
290
- if hasattr(target.payload, "model_dump")
291
- else target.payload
317
+ final_payload.model_dump()
318
+ if hasattr(final_payload, "model_dump")
319
+ else final_payload
292
320
  )
293
321
 
294
322
  scheduled_evt = Event(
@@ -301,10 +329,12 @@ class Engine:
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.1"
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