orca-runtime-python 0.1.24__tar.gz → 0.1.26__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.
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/PKG-INFO +1 -1
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/__init__.py +1 -1
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/machine.py +38 -20
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/parser.py +12 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/types.py +1 -1
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/PKG-INFO +1 -1
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/pyproject.toml +1 -1
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_guards.py +124 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/README.md +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/bus.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/effects.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/logging.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/persistence.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/SOURCES.txt +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/dependency_links.txt +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/requires.txt +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/top_level.txt +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/setup.cfg +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_actions_timeouts.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_ignored_events.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_md_parser.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_parallel.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_production_hardening.py +0 -0
- {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_snapshot_restore.py +0 -0
|
@@ -390,10 +390,10 @@ class OrcaMachine:
|
|
|
390
390
|
from_state=str(self._state),
|
|
391
391
|
)
|
|
392
392
|
|
|
393
|
-
# Find matching
|
|
394
|
-
|
|
393
|
+
# Find matching transitions (may be multiple with different guards)
|
|
394
|
+
candidates = self._find_transitions(evt)
|
|
395
395
|
|
|
396
|
-
if not
|
|
396
|
+
if not candidates:
|
|
397
397
|
# No transition found - event unhandled
|
|
398
398
|
return TransitionResult(
|
|
399
399
|
taken=False,
|
|
@@ -401,16 +401,28 @@ class OrcaMachine:
|
|
|
401
401
|
error=f"No transition for event {event_key} from {self._state.leaf()}"
|
|
402
402
|
)
|
|
403
403
|
|
|
404
|
-
#
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
404
|
+
# Try each candidate in order; first whose guard passes wins
|
|
405
|
+
transition = None
|
|
406
|
+
last_guard_name = None
|
|
407
|
+
for candidate in candidates:
|
|
408
|
+
if candidate.guard:
|
|
409
|
+
guard_passed = await self._evaluate_guard(candidate.guard)
|
|
410
|
+
if guard_passed:
|
|
411
|
+
transition = candidate
|
|
412
|
+
break
|
|
413
|
+
last_guard_name = candidate.guard
|
|
414
|
+
else:
|
|
415
|
+
# No guard — unconditional match
|
|
416
|
+
transition = candidate
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
if transition is None:
|
|
420
|
+
return TransitionResult(
|
|
421
|
+
taken=False,
|
|
422
|
+
from_state=str(self._state),
|
|
423
|
+
guard_failed=True,
|
|
424
|
+
error=f"Guard '{last_guard_name}' failed"
|
|
425
|
+
)
|
|
414
426
|
|
|
415
427
|
# Execute the transition
|
|
416
428
|
old_state = StateValue(self._state.value)
|
|
@@ -532,16 +544,20 @@ class OrcaMachine:
|
|
|
532
544
|
# Default fallback
|
|
533
545
|
return EventType.STATE_CHANGED
|
|
534
546
|
|
|
535
|
-
def
|
|
536
|
-
"""Find
|
|
547
|
+
def _find_transitions(self, event: Event) -> list[Transition]:
|
|
548
|
+
"""Find all transitions matching the current state and event."""
|
|
537
549
|
event_key = event.event_name or event.type.value
|
|
550
|
+
result: list[Transition] = []
|
|
538
551
|
|
|
539
552
|
# Check all active leaf states (important for parallel states)
|
|
540
553
|
for current in self._state.leaves():
|
|
541
554
|
# Try direct match on current leaf state
|
|
542
555
|
for t in self.definition.transitions:
|
|
543
556
|
if t.source == current and t.event == event_key:
|
|
544
|
-
|
|
557
|
+
result.append(t)
|
|
558
|
+
|
|
559
|
+
if result:
|
|
560
|
+
return result
|
|
545
561
|
|
|
546
562
|
# For compound states, also check parent's transitions
|
|
547
563
|
# Transitions on parent fire from any child
|
|
@@ -549,10 +565,12 @@ class OrcaMachine:
|
|
|
549
565
|
while parent:
|
|
550
566
|
for t in self.definition.transitions:
|
|
551
567
|
if t.source == parent and t.event == event_key:
|
|
552
|
-
|
|
568
|
+
result.append(t)
|
|
569
|
+
if result:
|
|
570
|
+
return result
|
|
553
571
|
parent = self._get_parent_state(parent)
|
|
554
572
|
|
|
555
|
-
return
|
|
573
|
+
return result
|
|
556
574
|
|
|
557
575
|
def _get_parent_state(self, state_name: str) -> str | None:
|
|
558
576
|
"""Get parent state name if state is nested (searches parallel regions too)."""
|
|
@@ -717,10 +735,10 @@ class OrcaMachine:
|
|
|
717
735
|
"""Resolve a ValueRef to its Python value."""
|
|
718
736
|
return ref.value
|
|
719
737
|
|
|
720
|
-
def _eval_compare(self, op: str, left: VariableRef, right: ValueRef) -> bool:
|
|
738
|
+
def _eval_compare(self, op: str, left: VariableRef, right: "ValueRef | VariableRef") -> bool:
|
|
721
739
|
"""Evaluate a comparison guard."""
|
|
722
740
|
lhs = self._resolve_variable(left)
|
|
723
|
-
rhs = self._resolve_value(right)
|
|
741
|
+
rhs = self._resolve_variable(right) if isinstance(right, VariableRef) else self._resolve_value(right)
|
|
724
742
|
|
|
725
743
|
# Try numeric comparison
|
|
726
744
|
try:
|
|
@@ -556,6 +556,14 @@ def _parse_guard_expression(input_str: str) -> GuardExpression:
|
|
|
556
556
|
return GuardNot(expr=parse_primary())
|
|
557
557
|
return parse_primary()
|
|
558
558
|
|
|
559
|
+
def _lookahead_is_var_path() -> bool:
|
|
560
|
+
"""Check if current position starts a variable path (ident.ident...)."""
|
|
561
|
+
return (
|
|
562
|
+
pos[0] + 1 < len(tokens)
|
|
563
|
+
and tokens[pos[0]].type == "ident"
|
|
564
|
+
and tokens[pos[0] + 1].type == "dot"
|
|
565
|
+
)
|
|
566
|
+
|
|
559
567
|
def parse_primary() -> GuardExpression:
|
|
560
568
|
tok = peek()
|
|
561
569
|
|
|
@@ -593,6 +601,10 @@ def _parse_guard_expression(input_str: str) -> GuardExpression:
|
|
|
593
601
|
# Comparison operator
|
|
594
602
|
if peek().type == "op":
|
|
595
603
|
op = advance().value
|
|
604
|
+
# RHS: variable path (ctx.foo) or literal value
|
|
605
|
+
if peek().type == "ident" and _lookahead_is_var_path():
|
|
606
|
+
right_ref = parse_var_path()
|
|
607
|
+
return GuardCompare(op=_map_op(op), left=var_path, right=right_ref)
|
|
596
608
|
right = parse_value()
|
|
597
609
|
# Special case: != null and == null
|
|
598
610
|
if right.type == "null":
|
|
@@ -12,6 +12,7 @@ from orca_runtime_python.types import (
|
|
|
12
12
|
GuardOr,
|
|
13
13
|
GuardNot,
|
|
14
14
|
GuardNullcheck,
|
|
15
|
+
VariableRef,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
18
|
|
|
@@ -126,6 +127,30 @@ def test_parse_string_compare():
|
|
|
126
127
|
assert guard.right.value == "pending", f"Expected value 'pending', got {guard.right.value}"
|
|
127
128
|
|
|
128
129
|
|
|
130
|
+
def test_parse_var_to_var_compare():
|
|
131
|
+
defn = parse_orca_md(orca_md("ctx.score >= ctx.threshold", "score"))
|
|
132
|
+
guard = defn.guards["g"]
|
|
133
|
+
assert isinstance(guard, GuardCompare), f"Expected GuardCompare, got {type(guard)}"
|
|
134
|
+
assert guard.op == "ge", f"Expected op 'ge', got '{guard.op}'"
|
|
135
|
+
assert guard.left.path == ["ctx", "score"]
|
|
136
|
+
assert isinstance(guard.right, VariableRef), f"Expected VariableRef on RHS, got {type(guard.right)}"
|
|
137
|
+
assert guard.right.path == ["ctx", "threshold"]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_parse_var_to_var_and():
|
|
141
|
+
defn = parse_orca_md(orca_md("ctx.a > ctx.b and ctx.c < ctx.d", "a"))
|
|
142
|
+
guard = defn.guards["g"]
|
|
143
|
+
assert isinstance(guard, GuardAnd), f"Expected GuardAnd, got {type(guard)}"
|
|
144
|
+
left = guard.left
|
|
145
|
+
right = guard.right
|
|
146
|
+
assert isinstance(left, GuardCompare)
|
|
147
|
+
assert isinstance(left.right, VariableRef)
|
|
148
|
+
assert left.right.path == ["ctx", "b"]
|
|
149
|
+
assert isinstance(right, GuardCompare)
|
|
150
|
+
assert isinstance(right.right, VariableRef)
|
|
151
|
+
assert right.right.path == ["ctx", "d"]
|
|
152
|
+
|
|
153
|
+
|
|
129
154
|
# ---- Evaluator tests ----
|
|
130
155
|
|
|
131
156
|
async def _test_eval_compare_pass():
|
|
@@ -252,6 +277,86 @@ async def _test_eval_false_literal():
|
|
|
252
277
|
assert result.guard_failed is True, f"Expected guard_failed"
|
|
253
278
|
|
|
254
279
|
|
|
280
|
+
def _orca_md_multi_ctx(guard_expr: str, fields: list[tuple[str, str, str]]) -> str:
|
|
281
|
+
"""Helper with multiple context fields: [(name, type, default), ...]."""
|
|
282
|
+
rows = "\n".join(f"| {n} | {t} | {d} |" for n, t, d in fields)
|
|
283
|
+
return f"""# machine test
|
|
284
|
+
|
|
285
|
+
## context
|
|
286
|
+
|
|
287
|
+
| Field | Type | Default |
|
|
288
|
+
|-------|------|---------|
|
|
289
|
+
{rows}
|
|
290
|
+
|
|
291
|
+
## events
|
|
292
|
+
|
|
293
|
+
- GO
|
|
294
|
+
|
|
295
|
+
## state idle [initial]
|
|
296
|
+
> Idle state
|
|
297
|
+
|
|
298
|
+
## state done [final]
|
|
299
|
+
> Done state
|
|
300
|
+
|
|
301
|
+
## guards
|
|
302
|
+
|
|
303
|
+
| Name | Expression |
|
|
304
|
+
|------|------------|
|
|
305
|
+
| g | `{guard_expr}` |
|
|
306
|
+
|
|
307
|
+
## transitions
|
|
308
|
+
|
|
309
|
+
| Source | Event | Target | Guard |
|
|
310
|
+
|--------|-------|--------|-------|
|
|
311
|
+
| idle | GO | done | g |
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
async def _test_eval_var_to_var_ge_pass():
|
|
316
|
+
defn = parse_orca_md(_orca_md_multi_ctx(
|
|
317
|
+
"ctx.score >= ctx.threshold",
|
|
318
|
+
[("score", "number", "95"), ("threshold", "number", "80")],
|
|
319
|
+
))
|
|
320
|
+
machine = OrcaMachine(defn, event_bus=EventBus())
|
|
321
|
+
await machine.start()
|
|
322
|
+
result = await machine.send("GO")
|
|
323
|
+
assert result.taken is True, f"Expected transition taken, got: {result.error}"
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
async def _test_eval_var_to_var_ge_fail():
|
|
327
|
+
defn = parse_orca_md(_orca_md_multi_ctx(
|
|
328
|
+
"ctx.score >= ctx.threshold",
|
|
329
|
+
[("score", "number", "0"), ("threshold", "number", "0")],
|
|
330
|
+
))
|
|
331
|
+
machine = OrcaMachine(defn, event_bus=EventBus(), context={"score": 50, "threshold": 80})
|
|
332
|
+
await machine.start()
|
|
333
|
+
result = await machine.send("GO")
|
|
334
|
+
assert result.taken is False, f"Expected transition NOT taken"
|
|
335
|
+
assert result.guard_failed is True, f"Expected guard_failed"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def _test_eval_var_to_var_lt_pass():
|
|
339
|
+
defn = parse_orca_md(_orca_md_multi_ctx(
|
|
340
|
+
"ctx.retry_count < ctx.max_retries",
|
|
341
|
+
[("retry_count", "number", "0"), ("max_retries", "number", "0")],
|
|
342
|
+
))
|
|
343
|
+
machine = OrcaMachine(defn, event_bus=EventBus(), context={"retry_count": 1, "max_retries": 3})
|
|
344
|
+
await machine.start()
|
|
345
|
+
result = await machine.send("GO")
|
|
346
|
+
assert result.taken is True, f"Expected transition taken, got: {result.error}"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
async def _test_eval_var_to_var_eq():
|
|
350
|
+
defn = parse_orca_md(_orca_md_multi_ctx(
|
|
351
|
+
"ctx.actual == ctx.expected",
|
|
352
|
+
[("actual", "number", "0"), ("expected", "number", "0")],
|
|
353
|
+
))
|
|
354
|
+
machine = OrcaMachine(defn, event_bus=EventBus(), context={"actual": 42, "expected": 42})
|
|
355
|
+
await machine.start()
|
|
356
|
+
result = await machine.send("GO")
|
|
357
|
+
assert result.taken is True, f"Expected transition taken, got: {result.error}"
|
|
358
|
+
|
|
359
|
+
|
|
255
360
|
# ---- Sync test wrappers ----
|
|
256
361
|
|
|
257
362
|
def test_eval_compare_pass():
|
|
@@ -299,6 +404,18 @@ def test_eval_true_literal():
|
|
|
299
404
|
def test_eval_false_literal():
|
|
300
405
|
asyncio.run(_test_eval_false_literal())
|
|
301
406
|
|
|
407
|
+
def test_eval_var_to_var_ge_pass():
|
|
408
|
+
asyncio.run(_test_eval_var_to_var_ge_pass())
|
|
409
|
+
|
|
410
|
+
def test_eval_var_to_var_ge_fail():
|
|
411
|
+
asyncio.run(_test_eval_var_to_var_ge_fail())
|
|
412
|
+
|
|
413
|
+
def test_eval_var_to_var_lt_pass():
|
|
414
|
+
asyncio.run(_test_eval_var_to_var_lt_pass())
|
|
415
|
+
|
|
416
|
+
def test_eval_var_to_var_eq():
|
|
417
|
+
asyncio.run(_test_eval_var_to_var_eq())
|
|
418
|
+
|
|
302
419
|
|
|
303
420
|
if __name__ == "__main__":
|
|
304
421
|
tests = [
|
|
@@ -329,6 +446,13 @@ if __name__ == "__main__":
|
|
|
329
446
|
("eval compare >= boundary", test_eval_compare_ge),
|
|
330
447
|
("eval true literal", test_eval_true_literal),
|
|
331
448
|
("eval false literal", test_eval_false_literal),
|
|
449
|
+
# Variable-to-variable comparison tests
|
|
450
|
+
("parse var-to-var compare", test_parse_var_to_var_compare),
|
|
451
|
+
("parse var-to-var and", test_parse_var_to_var_and),
|
|
452
|
+
("eval var-to-var >= pass (95 >= 80)", test_eval_var_to_var_ge_pass),
|
|
453
|
+
("eval var-to-var >= fail (50 >= 80)", test_eval_var_to_var_ge_fail),
|
|
454
|
+
("eval var-to-var < pass (1 < 3)", test_eval_var_to_var_lt_pass),
|
|
455
|
+
("eval var-to-var == pass (42 == 42)", test_eval_var_to_var_eq),
|
|
332
456
|
]
|
|
333
457
|
|
|
334
458
|
passed = 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/persistence.py
RENAMED
|
File without changes
|
{orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_production_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|