commandnet 0.5.1__tar.gz → 0.6.1__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.5.1
3
+ Version: 0.6.1
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
@@ -1,5 +1,5 @@
1
1
  from .core.models import Event
2
- from .core.node import Node, Parallel, ParallelTask, Schedule, Wait, Call, Interrupt
2
+ from .core.node import Node, Parallel, ParallelTask, Schedule, Wait, Call, Interrupt, Transition
3
3
  from .core.graph import GraphAnalyzer
4
4
  from .interfaces.persistence import Persistence
5
5
  from .interfaces.event_bus import EventBus
@@ -9,6 +9,6 @@ from .engine.runtime import Engine
9
9
  __all__ = [
10
10
  "Event", "Node", "Parallel", "ParallelTask", "GraphAnalyzer",
11
11
  "Persistence", "EventBus", "Observer", "Engine", "Schedule",
12
- "Call", "Interrupt",
12
+ "Call", "Interrupt", "Transition",
13
13
  ]
14
14
 
@@ -5,7 +5,7 @@ C = TypeVar('C', bound=BaseModel) # Context
5
5
  P = TypeVar('P', bound=BaseModel) # Payload
6
6
 
7
7
  # The Recursive Type Definition
8
- Target = Union[Type['Node'], 'Parallel', 'Schedule', 'Wait', 'Call', 'Interrupt', None]
8
+ Target = Union[Type['Node'], 'Parallel', 'Schedule', 'Wait', 'Call', 'Interrupt', 'Transition', None]
9
9
 
10
10
  class ParallelTask(BaseModel):
11
11
  model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -31,6 +31,11 @@ class Wait(BaseModel):
31
31
  resume_action: Target
32
32
  sub_context_path: Optional[str] = None
33
33
 
34
+ class Transition(BaseModel):
35
+ model_config = ConfigDict(arbitrary_types_allowed=True)
36
+ node_cls: Type['Node']
37
+ payload: Optional[Any] = None
38
+
34
39
  class Call(BaseModel):
35
40
  """The 'Await' type: Deduplicates execution based on an idempotency key."""
36
41
  model_config = ConfigDict(arbitrary_types_allowed=True)
@@ -8,6 +8,7 @@ from ..core.node import (
8
8
  Node,
9
9
  Parallel,
10
10
  Schedule,
11
+ Transition,
11
12
  Wait,
12
13
  Target,
13
14
  ParallelTask,
@@ -105,22 +106,15 @@ class Engine:
105
106
  if hasattr(ctx, "is_cancelled"):
106
107
  ctx.is_cancelled = await self.db.is_cancelled(subject_id)
107
108
 
108
- # 2. RELEASE LOCK before long-running compute
109
- await self.db.unlock_subject(subject_id)
110
-
111
- # 3. COMPUTE (no DB lock held)
109
+ # 2. COMPUTE (Lock is HELD via Redis during compute)
112
110
  start_t = asyncio.get_event_loop().time()
113
- result = await node_cls().run(ctx, payload)
111
+ try:
112
+ result = await node_cls().run(ctx, payload)
113
+ except asyncio.CancelledError:
114
+ self.logger.warning(f"Task {subject_id} cancelled during compute. Emitting Interrupt.")
115
+ result = Interrupt(subject_id=subject_id, hard=True)
116
+
114
117
  duration = (asyncio.get_event_loop().time() - start_t) * 1000
115
-
116
- # 4. RE-LOCK before writing
117
- node_name_check, _ = await self.db.lock_and_load(subject_id)
118
-
119
- # 🔴 IMPORTANT SAFETY CHECK (prevents stale writes)
120
- if node_name_check != event.node_name and node_name_check != "AWAITING_CALL":
121
- await self.db.unlock_subject(subject_id)
122
- return
123
-
124
118
  await self._apply_target(subject_id, ctx, result, duration)
125
119
 
126
120
  except Exception as e:
@@ -263,6 +257,22 @@ class Engine:
263
257
  subject_id, join_name, len(target.branches)
264
258
  )
265
259
 
260
+ async def _run_branch(task: ParallelTask):
261
+ branch_payload = (
262
+ task.payload if task.payload is not None else payload
263
+ )
264
+
265
+ sub_ctx = self._get_path(context, task.sub_context_path)
266
+
267
+ await self._apply_target(
268
+ f"{subject_id}#{task.sub_context_path}",
269
+ sub_ctx,
270
+ task.action,
271
+ payload=branch_payload,
272
+ )
273
+
274
+ branch_tasks = []
275
+
266
276
  for branch in target.branches:
267
277
  # Normalize branch into ParallelTask
268
278
  if isinstance(branch, ParallelTask):
@@ -275,19 +285,10 @@ class Engine:
275
285
  )
276
286
  task = ParallelTask(action=branch, sub_context_path=path)
277
287
 
278
- # PROPAGATION: fallback to incoming payload if branch has none
279
- branch_payload = (
280
- task.payload if task.payload is not None else payload
281
- )
288
+ branch_tasks.append(asyncio.create_task(_run_branch(task)))
282
289
 
283
- sub_ctx = self._get_path(context, task.sub_context_path)
284
-
285
- await self._apply_target(
286
- f"{subject_id}#{task.sub_context_path}",
287
- sub_ctx,
288
- task.action,
289
- payload=branch_payload,
290
- )
290
+ if branch_tasks:
291
+ await asyncio.gather(*branch_tasks)
291
292
 
292
293
  if target.join_node:
293
294
  await self.db.save_state(
@@ -346,6 +347,20 @@ class Engine:
346
347
  None,
347
348
  )
348
349
  return
350
+ if isinstance(target, Transition):
351
+ node_name = target.node_cls.get_node_name()
352
+ await self.observer.on_transition(subject_id, "RUN", node_name, duration)
353
+
354
+ # Use the transition's payload, or fallback to the current one
355
+ final_payload = target.payload if target.payload is not None else payload
356
+ p_load = final_payload.model_dump() if hasattr(final_payload, "model_dump") else final_payload
357
+
358
+ evt = Event(subject_id=subject_id, node_name=node_name, payload=p_load)
359
+ ctx_dict = self._dump_ctx(context)
360
+
361
+ await self.db.save_state(subject_id, node_name, ctx_dict, evt)
362
+ await self.bus.publish(evt)
363
+ return
349
364
  # --- EXTERNAL CONTROL & SIGNALS ---
350
365
 
351
366
  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.5.1"
3
+ version = "0.6.1"
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