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.
Files changed (24) hide show
  1. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/PKG-INFO +1 -1
  2. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/__init__.py +1 -1
  3. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/machine.py +38 -20
  4. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/parser.py +12 -0
  5. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/types.py +1 -1
  6. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/PKG-INFO +1 -1
  7. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/pyproject.toml +1 -1
  8. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_guards.py +124 -0
  9. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/README.md +0 -0
  10. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/bus.py +0 -0
  11. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/effects.py +0 -0
  12. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/logging.py +0 -0
  13. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python/persistence.py +0 -0
  14. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/SOURCES.txt +0 -0
  15. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/dependency_links.txt +0 -0
  16. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/requires.txt +0 -0
  17. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/orca_runtime_python.egg-info/top_level.txt +0 -0
  18. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/setup.cfg +0 -0
  19. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_actions_timeouts.py +0 -0
  20. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_ignored_events.py +0 -0
  21. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_md_parser.py +0 -0
  22. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_parallel.py +0 -0
  23. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_production_hardening.py +0 -0
  24. {orca_runtime_python-0.1.24 → orca_runtime_python-0.1.26}/tests/test_snapshot_restore.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orca-runtime-python
3
- Version: 0.1.24
3
+ Version: 0.1.26
4
4
  Summary: Python async runtime for Orca state machines
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/orca-lang/orca-lang
@@ -33,7 +33,7 @@ from .persistence import PersistenceAdapter, AsyncPersistenceAdapter, FilePersis
33
33
 
34
34
  from .logging import LogSink, FileSink, ConsoleSink, MultiSink
35
35
 
36
- __version__ = "0.1.0"
36
+ __version__ = "0.1.26"
37
37
 
38
38
  __all__ = [
39
39
  # Types
@@ -390,10 +390,10 @@ class OrcaMachine:
390
390
  from_state=str(self._state),
391
391
  )
392
392
 
393
- # Find matching transition
394
- transition = self._find_transition(evt)
393
+ # Find matching transitions (may be multiple with different guards)
394
+ candidates = self._find_transitions(evt)
395
395
 
396
- if not transition:
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
- # Evaluate guard if present
405
- if transition.guard:
406
- guard_passed = await self._evaluate_guard(transition.guard)
407
- if not guard_passed:
408
- return TransitionResult(
409
- taken=False,
410
- from_state=str(self._state),
411
- guard_failed=True,
412
- error=f"Guard '{transition.guard}' failed"
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 _find_transition(self, event: Event) -> Transition | None:
536
- """Find a transition matching the current state and event."""
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
- return t
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
- return t
568
+ result.append(t)
569
+ if result:
570
+ return result
553
571
  parent = self._get_parent_state(parent)
554
572
 
555
- return None
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":
@@ -124,7 +124,7 @@ class GuardCompare(GuardExpression):
124
124
  """Comparison guard: left op right"""
125
125
  op: str # eq, ne, lt, gt, le, ge
126
126
  left: VariableRef
127
- right: ValueRef
127
+ right: "ValueRef | VariableRef"
128
128
 
129
129
 
130
130
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orca-runtime-python
3
- Version: 0.1.24
3
+ Version: 0.1.26
4
4
  Summary: Python async runtime for Orca state machines
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/orca-lang/orca-lang
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "orca-runtime-python"
7
- version = "0.1.24"
7
+ version = "0.1.26"
8
8
  description = "Python async runtime for Orca state machines"
9
9
  readme = "README.md"
10
10
  license = {text = "Apache-2.0"}
@@ -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