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.
- {commandnet-0.5.1 → commandnet-0.6.1}/PKG-INFO +1 -1
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/__init__.py +2 -2
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/core/node.py +6 -1
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/engine/runtime.py +41 -26
- {commandnet-0.5.1 → commandnet-0.6.1}/pyproject.toml +1 -1
- {commandnet-0.5.1 → commandnet-0.6.1}/README.md +0 -0
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/core/graph.py +0 -0
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/core/models.py +0 -0
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/interfaces/event_bus.py +0 -0
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/interfaces/observer.py +0 -0
- {commandnet-0.5.1 → commandnet-0.6.1}/commandnet/interfaces/persistence.py +0 -0
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|