vention-state-machine 0.4__tar.gz → 0.4.4__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: vention-state-machine
3
- Version: 0.4
3
+ Version: 0.4.4
4
4
  Summary: Declarative state machine framework for machine apps
5
5
  License: Proprietary
6
6
  Author: VentionCo
@@ -8,12 +8,28 @@ Requires-Python: >=3.10,<3.11
8
8
  Classifier: License :: Other/Proprietary License
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.10
11
- Requires-Dist: asyncio (>=3.4.3,<4.0.0)
12
- Requires-Dist: coverage (>=7.10.1,<8.0.0)
13
- Requires-Dist: graphviz (>=0.21,<0.22)
14
- Requires-Dist: transitions (>=0.9.3,<0.10.0)
15
- Requires-Dist: uvicorn (>=0.35.0,<0.36.0)
16
- Requires-Dist: vention-communication (>=0.2.2,<0.3.0)
11
+ Requires-Dist: annotated-doc (==0.0.4) ; python_version == "3.10"
12
+ Requires-Dist: annotated-types (==0.7.0) ; python_version == "3.10"
13
+ Requires-Dist: anyio (==4.11.0) ; python_version == "3.10"
14
+ Requires-Dist: asyncio (==3.4.3) ; python_version == "3.10"
15
+ Requires-Dist: click (==8.1.8) ; python_version == "3.10"
16
+ Requires-Dist: colorama (==0.4.6) ; python_version == "3.10" and platform_system == "Windows"
17
+ Requires-Dist: coverage (==7.10.7) ; python_version == "3.10"
18
+ Requires-Dist: exceptiongroup (==1.3.0) ; python_version == "3.10"
19
+ Requires-Dist: fastapi (==0.121.1) ; python_version == "3.10"
20
+ Requires-Dist: graphviz (==0.21) ; python_version == "3.10"
21
+ Requires-Dist: h11 (==0.16.0) ; python_version == "3.10"
22
+ Requires-Dist: idna (==3.11) ; python_version == "3.10"
23
+ Requires-Dist: pydantic (==2.12.3) ; python_version == "3.10"
24
+ Requires-Dist: pydantic-core (==2.41.4) ; python_version == "3.10"
25
+ Requires-Dist: six (==1.17.0) ; python_version == "3.10"
26
+ Requires-Dist: sniffio (==1.3.1) ; python_version == "3.10"
27
+ Requires-Dist: starlette (==0.48.0) ; python_version == "3.10"
28
+ Requires-Dist: transitions (==0.9.3) ; python_version == "3.10"
29
+ Requires-Dist: typing-extensions (==4.15.0) ; python_version == "3.10"
30
+ Requires-Dist: typing-inspection (==0.4.2) ; python_version == "3.10"
31
+ Requires-Dist: uvicorn (==0.35.0) ; python_version == "3.10"
32
+ Requires-Dist: vention-communication (==0.3.0) ; python_version == "3.10"
17
33
  Description-Content-Type: text/markdown
18
34
 
19
35
  # vention-state-machine
@@ -0,0 +1,130 @@
1
+ [tool.poetry]
2
+ name = "vention-state-machine"
3
+ version = "0.4.4"
4
+ description = "Declarative state machine framework for machine apps"
5
+ authors = [ "VentionCo" ]
6
+ readme = "README.md"
7
+ license = "Proprietary"
8
+
9
+ [[tool.poetry.packages]]
10
+ include = "state_machine"
11
+ from = "src"
12
+
13
+ [tool.poetry.build]
14
+ generate-setup-file = false
15
+
16
+ [tool.poetry.dependencies]
17
+ python = ">=3.10,<3.11"
18
+
19
+ [tool.poetry.dependencies.annotated-doc]
20
+ version = "0.0.4"
21
+ markers = 'python_version == "3.10"'
22
+ optional = false
23
+
24
+ [tool.poetry.dependencies.annotated-types]
25
+ version = "0.7.0"
26
+ markers = 'python_version == "3.10"'
27
+ optional = false
28
+
29
+ [tool.poetry.dependencies.anyio]
30
+ version = "4.11.0"
31
+ markers = 'python_version == "3.10"'
32
+ optional = false
33
+
34
+ [tool.poetry.dependencies.asyncio]
35
+ version = "3.4.3"
36
+ markers = 'python_version == "3.10"'
37
+ optional = false
38
+
39
+ [tool.poetry.dependencies.click]
40
+ version = "8.1.8"
41
+ markers = 'python_version == "3.10"'
42
+ optional = false
43
+
44
+ [tool.poetry.dependencies.colorama]
45
+ version = "0.4.6"
46
+ markers = 'python_version == "3.10" and platform_system == "Windows"'
47
+ optional = false
48
+
49
+ [tool.poetry.dependencies.coverage]
50
+ version = "7.10.7"
51
+ markers = 'python_version == "3.10"'
52
+ optional = false
53
+
54
+ [tool.poetry.dependencies.exceptiongroup]
55
+ version = "1.3.0"
56
+ markers = 'python_version == "3.10"'
57
+ optional = false
58
+
59
+ [tool.poetry.dependencies.fastapi]
60
+ version = "0.121.1"
61
+ markers = 'python_version == "3.10"'
62
+ optional = false
63
+
64
+ [tool.poetry.dependencies.graphviz]
65
+ version = "0.21"
66
+ markers = 'python_version == "3.10"'
67
+ optional = false
68
+
69
+ [tool.poetry.dependencies.h11]
70
+ version = "0.16.0"
71
+ markers = 'python_version == "3.10"'
72
+ optional = false
73
+
74
+ [tool.poetry.dependencies.idna]
75
+ version = "3.11"
76
+ markers = 'python_version == "3.10"'
77
+ optional = false
78
+
79
+ [tool.poetry.dependencies.pydantic-core]
80
+ version = "2.41.4"
81
+ markers = 'python_version == "3.10"'
82
+ optional = false
83
+
84
+ [tool.poetry.dependencies.pydantic]
85
+ version = "2.12.3"
86
+ markers = 'python_version == "3.10"'
87
+ optional = false
88
+
89
+ [tool.poetry.dependencies.six]
90
+ version = "1.17.0"
91
+ markers = 'python_version == "3.10"'
92
+ optional = false
93
+
94
+ [tool.poetry.dependencies.sniffio]
95
+ version = "1.3.1"
96
+ markers = 'python_version == "3.10"'
97
+ optional = false
98
+
99
+ [tool.poetry.dependencies.starlette]
100
+ version = "0.48.0"
101
+ markers = 'python_version == "3.10"'
102
+ optional = false
103
+
104
+ [tool.poetry.dependencies.transitions]
105
+ version = "0.9.3"
106
+ markers = 'python_version == "3.10"'
107
+ optional = false
108
+
109
+ [tool.poetry.dependencies.typing-extensions]
110
+ version = "4.15.0"
111
+ markers = 'python_version == "3.10"'
112
+ optional = false
113
+
114
+ [tool.poetry.dependencies.typing-inspection]
115
+ version = "0.4.2"
116
+ markers = 'python_version == "3.10"'
117
+ optional = false
118
+
119
+ [tool.poetry.dependencies.uvicorn]
120
+ version = "0.35.0"
121
+ markers = 'python_version == "3.10"'
122
+ optional = false
123
+
124
+ [tool.poetry.dependencies.vention-communication]
125
+ version = "0.3.0"
126
+ markers = 'python_version == "3.10"'
127
+ optional = false
128
+
129
+ [tool.poetry.group.dev]
130
+ dependencies = { }
@@ -58,19 +58,12 @@ class StateMachine(HierarchicalGraphMachine):
58
58
  ) -> None:
59
59
  # Normalize state definitions
60
60
  if is_state_container(states):
61
- state_groups = [
62
- value
63
- for value in vars(states).values()
64
- if isinstance(value, StateGroup)
65
- ]
61
+ state_groups = [value for value in vars(states).values() if isinstance(value, StateGroup)]
66
62
  resolved_states = [group.to_state_list()[0] for group in state_groups]
67
63
  elif isinstance(states, list):
68
64
  resolved_states = states
69
65
  else:
70
- raise TypeError(
71
- f"`states` must be either a StateGroup container or a list of state dicts. "
72
- f"Got: {type(states).__name__}"
73
- )
66
+ raise TypeError(f"`states` must be either a StateGroup container or a list of state dicts. " f"Got: {type(states).__name__}")
74
67
 
75
68
  self._declared_states: list[dict[str, Any]] = resolved_states
76
69
 
@@ -95,9 +88,7 @@ class StateMachine(HierarchicalGraphMachine):
95
88
  self._timeouts: dict[str, asyncio.Task[Any]] = {}
96
89
  self._last_state: Optional[str] = None
97
90
  self._enable_recovery: bool = enable_last_state_recovery
98
- self._history: deque[dict[str, Any]] = deque(
99
- maxlen=history_size or self.DEFAULT_HISTORY_SIZE
100
- )
91
+ self._history: deque[dict[str, Any]] = deque(maxlen=history_size or self.DEFAULT_HISTORY_SIZE)
101
92
  self._current_start: Optional[datetime] = None
102
93
  self._guard_conditions: dict[str, List[Callable[[], bool]]] = {}
103
94
  self._state_change_callbacks: List[Callable[[str, str, str], None]] = []
@@ -110,9 +101,7 @@ class StateMachine(HierarchicalGraphMachine):
110
101
  BaseStates.FAULT.value,
111
102
  before="cancel_tasks",
112
103
  )
113
- self.add_transition(
114
- BaseTriggers.RESET.value, BaseStates.FAULT.value, BaseStates.READY.value
115
- )
104
+ self.add_transition(BaseTriggers.RESET.value, BaseStates.FAULT.value, BaseStates.READY.value)
116
105
  self._attach_after_hooks()
117
106
  self._add_guard_conditions_to_transitions()
118
107
 
@@ -137,9 +126,7 @@ class StateMachine(HierarchicalGraphMachine):
137
126
  except asyncio.CancelledError:
138
127
  pass
139
128
 
140
- def set_timeout(
141
- self, state_name: str, seconds: float, trigger_fn: Callable[[], str]
142
- ) -> None:
129
+ def set_timeout(self, state_name: str, seconds: float, trigger_fn: Callable[[], str]) -> None:
143
130
  """
144
131
  Schedule a timeout: if `state_name` remains active after `seconds`, fire `trigger_fn()`.
145
132
  """
@@ -191,9 +178,7 @@ class StateMachine(HierarchicalGraphMachine):
191
178
  """Get the last `n` history entries."""
192
179
  return list(self._history)[-n:] if n > 0 else []
193
180
 
194
- def add_transition_condition(
195
- self, trigger_name: str, condition_fn: Callable[[], bool]
196
- ) -> None:
181
+ def add_transition_condition(self, trigger_name: str, condition_fn: Callable[[], bool]) -> None:
197
182
  """
198
183
  Add a guard condition to a transition trigger.
199
184
  Multiple conditions can be added for the same trigger - ALL must pass for the transition to be allowed.
@@ -202,9 +187,7 @@ class StateMachine(HierarchicalGraphMachine):
202
187
  self._guard_conditions[trigger_name] = []
203
188
  self._guard_conditions[trigger_name].append(condition_fn)
204
189
 
205
- def add_state_change_callback(
206
- self, callback: Callable[[str, str, str], None]
207
- ) -> None:
190
+ def add_state_change_callback(self, callback: Callable[[str, str, str], None]) -> None:
208
191
  """
209
192
  Add a callback that fires on any state change.
210
193
 
@@ -38,14 +38,10 @@ class DecoratorManager:
38
38
  def _discover_state_callbacks(self, callback_fn: Any) -> None:
39
39
  """Discover on_enter_state and on_exit_state decorators."""
40
40
  if hasattr(callback_fn, "_on_enter_state"):
41
- self._decorator_bindings.append(
42
- (callback_fn._on_enter_state, "enter", callback_fn)
43
- )
41
+ self._decorator_bindings.append((callback_fn._on_enter_state, "enter", callback_fn))
44
42
 
45
43
  if hasattr(callback_fn, "_on_exit_state"):
46
- self._exit_decorator_bindings.append(
47
- (callback_fn._on_exit_state, "exit", callback_fn)
48
- )
44
+ self._exit_decorator_bindings.append((callback_fn._on_exit_state, "exit", callback_fn))
49
45
 
50
46
  def _discover_guard_conditions(self, callback_fn: Any) -> None:
51
47
  """Discover guard decorators."""
@@ -70,9 +66,7 @@ class DecoratorManager:
70
66
 
71
67
  def _bind_state_callbacks(self, instance: Any) -> None:
72
68
  """Bind state entry/exit callbacks."""
73
- for state_name, hook_type, callback_fn in (
74
- self._decorator_bindings + self._exit_decorator_bindings
75
- ):
69
+ for state_name, hook_type, callback_fn in self._decorator_bindings + self._exit_decorator_bindings:
76
70
  bound_fn = callback_fn.__get__(instance)
77
71
 
78
72
  if hook_type == "enter" and hasattr(callback_fn, "_timeout_config"):
@@ -54,9 +54,7 @@ def on_exit_state(
54
54
  return decorator
55
55
 
56
56
 
57
- def auto_timeout(
58
- seconds: float, trigger: Union[str, Callable[[], str]] = "to_fault"
59
- ) -> Callable[[CallableType], CallableType]:
57
+ def auto_timeout(seconds: float, trigger: Union[str, Callable[[], str]] = "to_fault") -> Callable[[CallableType], CallableType]:
60
58
  """
61
59
  Decorator that applies an auto-timeout configuration to a state entry handler.
62
60
  """
@@ -90,11 +88,7 @@ def guard(
90
88
  guard_fn._guard_conditions = {}
91
89
 
92
90
  for trigger in triggers:
93
- trigger_name = (
94
- getattr(trigger, "name", trigger)
95
- if hasattr(trigger, "name")
96
- else trigger
97
- )
91
+ trigger_name = getattr(trigger, "name", trigger) if hasattr(trigger, "name") else trigger
98
92
  if not isinstance(trigger_name, str):
99
93
  raise TypeError(f"Expected a Trigger or str, got {type(trigger)}")
100
94
 
@@ -88,9 +88,7 @@ class Trigger:
88
88
  def __call__(self) -> str:
89
89
  return self.name
90
90
 
91
- def transition(
92
- self, source: Union[str, State], dest: Union[str, State]
93
- ) -> Dict[str, str]:
91
+ def transition(self, source: Union[str, State], dest: Union[str, State]) -> Dict[str, str]:
94
92
  """
95
93
  Build {"trigger": self.name, "source": <src>, "dest": <dst>}.
96
94
  `source` / `dest` may be raw strings or StateKey instances.
@@ -2,9 +2,7 @@ from typing import Protocol, Callable, Any
2
2
 
3
3
 
4
4
  class SupportsTimeout(Protocol):
5
- def set_timeout(
6
- self, state_name: str, seconds: float, trigger_fn: Callable[[], str]
7
- ) -> None: ...
5
+ def set_timeout(self, state_name: str, seconds: float, trigger_fn: Callable[[], str]) -> None: ...
8
6
 
9
7
 
10
8
  class SupportsStateCallbacks(Protocol):
@@ -12,15 +10,11 @@ class SupportsStateCallbacks(Protocol):
12
10
 
13
11
 
14
12
  class SupportsGuardConditions(Protocol):
15
- def add_transition_condition(
16
- self, trigger_name: str, condition_fn: Callable[[], bool]
17
- ) -> None: ...
13
+ def add_transition_condition(self, trigger_name: str, condition_fn: Callable[[], bool]) -> None: ...
18
14
 
19
15
 
20
16
  class SupportsStateChangeCallbacks(Protocol):
21
- def add_state_change_callback(
22
- self, callback: Callable[[str, str, str], None]
23
- ) -> None: ...
17
+ def add_state_change_callback(self, callback: Callable[[str, str, str], None]) -> None: ...
24
18
 
25
19
 
26
20
  class StateMachineProtocol(
@@ -48,11 +48,8 @@ def is_state_container(state_source: object) -> bool:
48
48
  Return True if `obj` is an instance or class that contains StateGroup(s).
49
49
  """
50
50
  try:
51
- return any(
52
- isinstance(value, StateGroup) for value in vars(state_source).values()
53
- ) or any(
54
- isinstance(value, StateGroup)
55
- for value in vars(state_source.__class__).values()
51
+ return any(isinstance(value, StateGroup) for value in vars(state_source).values()) or any(
52
+ isinstance(value, StateGroup) for value in vars(state_source.__class__).values()
56
53
  )
57
54
  except TypeError:
58
55
  return False
@@ -15,11 +15,11 @@ from state_machine.utils import (
15
15
  )
16
16
 
17
17
 
18
- __all__ = ["build_state_machine_bundle"]
18
+ __all__ = ["build_state_machine_rpc_bundle"]
19
19
 
20
20
 
21
- def build_state_machine_bundle(
22
- sm: StateMachine,
21
+ def build_state_machine_rpc_bundle(
22
+ state_machine: StateMachine,
23
23
  *,
24
24
  include_state_actions: bool = True,
25
25
  include_history_action: bool = True,
@@ -44,8 +44,8 @@ def build_state_machine_bundle(
44
44
  if include_state_actions:
45
45
 
46
46
  async def get_state() -> StateResponse:
47
- last = getattr(sm, "get_last_state", lambda: None)()
48
- return StateResponse(state=sm.state, last_state=last)
47
+ last = getattr(state_machine, "get_last_state", lambda: None)()
48
+ return StateResponse(state=state_machine.state, last_state=last)
49
49
 
50
50
  bundle.actions.append(
51
51
  ActionEntry(
@@ -62,17 +62,15 @@ def build_state_machine_bundle(
62
62
  if include_history_action:
63
63
 
64
64
  async def get_history() -> HistoryResponse:
65
- raw = getattr(sm, "history", [])
65
+ raw = getattr(state_machine, "history", [])
66
66
  out = []
67
67
 
68
68
  for item in raw:
69
69
  if isinstance(item, dict):
70
- # Handle dict items from history
71
70
  state_str = str(item.get("state", ""))
72
71
  duration_ms: Optional[int] = item.get("duration_ms")
73
72
  if duration_ms is not None and not isinstance(duration_ms, int):
74
73
  duration_ms = None
75
- # Add timestamp if present, otherwise use current time
76
74
  timestamp: datetime
77
75
  if "timestamp" in item and isinstance(item["timestamp"], datetime):
78
76
  timestamp = item["timestamp"]
@@ -86,7 +84,6 @@ def build_state_machine_bundle(
86
84
  )
87
85
  )
88
86
  else:
89
- # Handle simple state strings
90
87
  out.append(HistoryEntry(state=str(item), timestamp=datetime.now()))
91
88
 
92
89
  return HistoryResponse(history=out, buffer_size=len(raw))
@@ -103,8 +100,8 @@ def build_state_machine_bundle(
103
100
  # ======================================================
104
101
  # Triggers
105
102
  # ======================================================
106
- # Source of truth: transitions registered on SM
107
- all_triggers = sorted(sm.events.keys())
103
+ # Source of truth: transitions registered on state machine
104
+ all_triggers = sorted(state_machine.events.keys())
108
105
  selected = all_triggers if triggers is None else list(triggers)
109
106
 
110
107
  # ---------- factory function (fixes closure bug) ----------
@@ -112,22 +109,19 @@ def build_state_machine_bundle(
112
109
  trigger_name: str,
113
110
  ) -> Callable[[], Coroutine[Any, Any, TriggerResponse]]:
114
111
  async def trigger_call() -> TriggerResponse:
115
- current = sm.state
116
- allowed = sm.get_triggers(current)
112
+ current = state_machine.state
113
+ allowed = state_machine.get_triggers(current)
117
114
 
118
115
  # Precondition error if invalid in current state
119
116
  if trigger_name not in allowed:
120
117
  raise ConnectError(
121
118
  code="failed_precondition",
122
- message=(
123
- f"Trigger '{trigger_name}' cannot run from '{current}'. "
124
- f"Allowed: {sorted(allowed)}"
125
- ),
119
+ message=(f"Trigger '{trigger_name}' cannot run from '{current}'. " f"Allowed: {sorted(allowed)}"),
126
120
  )
127
121
 
128
122
  # Fetch bound trigger method
129
123
  try:
130
- method = getattr(sm, trigger_name)
124
+ method = getattr(state_machine, trigger_name)
131
125
  except AttributeError as e:
132
126
  raise ConnectError(
133
127
  code="internal",
@@ -148,7 +142,7 @@ def build_state_machine_bundle(
148
142
  return TriggerResponse(
149
143
  result=trigger_name,
150
144
  previous_state=current,
151
- new_state=sm.state,
145
+ new_state=state_machine.state,
152
146
  )
153
147
 
154
148
  return trigger_call
@@ -1,21 +0,0 @@
1
- [tool.poetry]
2
- name = "vention-state-machine"
3
- version = "0.4"
4
- description = "Declarative state machine framework for machine apps"
5
- authors = [ "VentionCo" ]
6
- readme = "README.md"
7
- license = "Proprietary"
8
- packages = [{ include = "state_machine", from = "src" }]
9
-
10
- [tool.poetry.dependencies]
11
- asyncio = "^3.4.3"
12
- python = ">=3.10,<3.11"
13
- uvicorn = "^0.35.0"
14
- transitions = "^0.9.3"
15
- graphviz = "^0.21"
16
- coverage = "^7.10.1"
17
- vention-communication = "^0.2.2"
18
-
19
- [tool.poetry.group.dev.dependencies]
20
- pytest = "^8.3.4"
21
- ruff = "^0.8.0"