commandnet 0.5.0__tar.gz → 0.6.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.
- {commandnet-0.5.0 → commandnet-0.6.0}/PKG-INFO +1 -1
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/__init__.py +2 -2
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/core/node.py +6 -1
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/engine/runtime.py +36 -10
- {commandnet-0.5.0 → commandnet-0.6.0}/pyproject.toml +1 -1
- {commandnet-0.5.0 → commandnet-0.6.0}/README.md +0 -0
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/core/graph.py +0 -0
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/core/models.py +0 -0
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/interfaces/event_bus.py +0 -0
- {commandnet-0.5.0 → commandnet-0.6.0}/commandnet/interfaces/observer.py +0 -0
- {commandnet-0.5.0 → commandnet-0.6.0}/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,
|
|
@@ -70,8 +71,10 @@ class Engine:
|
|
|
70
71
|
|
|
71
72
|
async def _run_node_logic(self, event: Event):
|
|
72
73
|
subject_id = event.subject_id
|
|
73
|
-
|
|
74
|
+
|
|
75
|
+
# 1. READ & LOCK
|
|
74
76
|
node_name, ctx_dict = await self.db.lock_and_load(subject_id)
|
|
77
|
+
|
|
75
78
|
if not node_name or (
|
|
76
79
|
node_name != event.node_name and node_name != "AWAITING_CALL"
|
|
77
80
|
):
|
|
@@ -92,33 +95,42 @@ class Engine:
|
|
|
92
95
|
if hasattr(ctx_type, "model_validate")
|
|
93
96
|
else ctx_dict
|
|
94
97
|
)
|
|
98
|
+
|
|
95
99
|
payload = (
|
|
96
100
|
payload_type.model_validate(event.payload)
|
|
97
101
|
if (event.payload and hasattr(payload_type, "model_validate"))
|
|
98
102
|
else event.payload
|
|
99
103
|
)
|
|
100
104
|
|
|
101
|
-
#
|
|
105
|
+
# Soft cancel check BEFORE compute
|
|
102
106
|
if hasattr(ctx, "is_cancelled"):
|
|
103
107
|
ctx.is_cancelled = await self.db.is_cancelled(subject_id)
|
|
104
108
|
|
|
109
|
+
# 2. RELEASE LOCK before long-running compute
|
|
110
|
+
await self.db.unlock_subject(subject_id)
|
|
111
|
+
|
|
112
|
+
# 3. COMPUTE (no DB lock held)
|
|
105
113
|
start_t = asyncio.get_event_loop().time()
|
|
106
114
|
result = await node_cls().run(ctx, payload)
|
|
115
|
+
duration = (asyncio.get_event_loop().time() - start_t) * 1000
|
|
116
|
+
|
|
117
|
+
# 4. RE-LOCK before writing
|
|
118
|
+
node_name_check, _ = await self.db.lock_and_load(subject_id)
|
|
119
|
+
|
|
120
|
+
# 🔴 IMPORTANT SAFETY CHECK (prevents stale writes)
|
|
121
|
+
if node_name_check != event.node_name and node_name_check != "AWAITING_CALL":
|
|
122
|
+
await self.db.unlock_subject(subject_id)
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
await self._apply_target(subject_id, ctx, result, duration)
|
|
107
126
|
|
|
108
|
-
await self._apply_target(
|
|
109
|
-
subject_id,
|
|
110
|
-
ctx,
|
|
111
|
-
result,
|
|
112
|
-
(asyncio.get_event_loop().time() - start_t) * 1000,
|
|
113
|
-
)
|
|
114
127
|
except Exception as e:
|
|
115
128
|
await self.observer.on_error(subject_id, event.node_name, e)
|
|
116
129
|
raise
|
|
117
130
|
finally:
|
|
131
|
+
# Ensure we never leave a lock hanging
|
|
118
132
|
await self.db.unlock_subject(subject_id)
|
|
119
133
|
|
|
120
|
-
# --- RECURSIVE TARGET RESOLVER ---
|
|
121
|
-
|
|
122
134
|
async def _apply_target(
|
|
123
135
|
self,
|
|
124
136
|
subject_id: str,
|
|
@@ -335,6 +347,20 @@ class Engine:
|
|
|
335
347
|
None,
|
|
336
348
|
)
|
|
337
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
|
|
338
364
|
# --- EXTERNAL CONTROL & SIGNALS ---
|
|
339
365
|
|
|
340
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.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|