semantic-state-machine 0.1.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.
- semantic_state_machine-0.1.0/PKG-INFO +129 -0
- semantic_state_machine-0.1.0/README.md +110 -0
- semantic_state_machine-0.1.0/pyproject.toml +42 -0
- semantic_state_machine-0.1.0/src/semantic_state_machine/__init__.py +17 -0
- semantic_state_machine-0.1.0/src/semantic_state_machine/py.typed +0 -0
- semantic_state_machine-0.1.0/src/semantic_state_machine/state_machine.py +180 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: semantic-state-machine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight, type-safe Python state machine implementation using modern Python 3.13+ features.
|
|
5
|
+
Keywords: state-machine,finite-state-machine,type-safety,audit
|
|
6
|
+
Author: Richard West
|
|
7
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
|
+
Requires-Python: >=3.13
|
|
15
|
+
Project-URL: Homepage, https://github.com/rjdw/semantic-state-machine
|
|
16
|
+
Project-URL: Repository, https://github.com/rjdw/semantic-state-machine
|
|
17
|
+
Project-URL: Issues, https://github.com/rjdw/semantic-state-machine/issues
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# State Machine
|
|
21
|
+
|
|
22
|
+
A lightweight, type-safe Python state machine implementation using modern Python 3.13+ features like PEP 695 type parameters.
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Type Safety**: Leverage Python's type system to ensure consistent states, events, and contexts.
|
|
27
|
+
- **Decorator Support**: Easily define transitions using the `@sm.transition` decorator.
|
|
28
|
+
- **Auditing**: Built-in support for recording transition history via `AuditedStateMachine` and `AuditContext`.
|
|
29
|
+
- **Flexible Context**: Pass a custom context object to transition actions to maintain state outside the machine.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
This project uses `uv` for dependency management. To get started, ensure you have `uv` installed and run:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uv sync
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
You can install the package via pip:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install semantic-state-machine
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
### Basic State Machine
|
|
48
|
+
|
|
49
|
+
Define your states and events as `Enum`s and create a context class.
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from enum import Enum
|
|
53
|
+
from semantic_state_machine import StateMachine
|
|
54
|
+
|
|
55
|
+
class State(Enum):
|
|
56
|
+
LOCKED = 1
|
|
57
|
+
UNLOCKED = 2
|
|
58
|
+
|
|
59
|
+
class Event(Enum):
|
|
60
|
+
PUSH = 1
|
|
61
|
+
COIN = 2
|
|
62
|
+
|
|
63
|
+
class TurnstileContext:
|
|
64
|
+
def __init__(self):
|
|
65
|
+
self.total_coins = 0
|
|
66
|
+
|
|
67
|
+
sm = StateMachine[State, Event, TurnstileContext]()
|
|
68
|
+
|
|
69
|
+
@sm.transition(State.LOCKED, Event.COIN, State.UNLOCKED)
|
|
70
|
+
def insert_coin(ctx: TurnstileContext):
|
|
71
|
+
ctx.total_coins += 1
|
|
72
|
+
print("Coin inserted. Turnstile unlocked.")
|
|
73
|
+
|
|
74
|
+
@sm.transition(State.UNLOCKED, Event.PUSH, State.LOCKED)
|
|
75
|
+
def push_turnstile(ctx: TurnstileContext):
|
|
76
|
+
print("Turnstile pushed. Turnstile locked.")
|
|
77
|
+
|
|
78
|
+
# Usage
|
|
79
|
+
ctx = TurnstileContext()
|
|
80
|
+
current_state = State.LOCKED
|
|
81
|
+
current_state = sm.handle_transition(ctx, current_state, Event.COIN)
|
|
82
|
+
# current_state is now State.UNLOCKED
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Audited State Machine
|
|
86
|
+
|
|
87
|
+
Use `AuditedStateMachine` and `AuditContext` to automatically track the history of transitions.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from semantic_state_machine import AuditedStateMachine, AuditContext
|
|
91
|
+
|
|
92
|
+
class MyContext(AuditContext[State, Event]):
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
sm = AuditedStateMachine[State, Event, MyContext]()
|
|
96
|
+
|
|
97
|
+
# Transitions are defined the same way
|
|
98
|
+
@sm.transition(State.LOCKED, Event.COIN, State.UNLOCKED)
|
|
99
|
+
def insert_coin(ctx: MyContext):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
ctx = MyContext()
|
|
103
|
+
sm.handle_transition(ctx, State.LOCKED, Event.COIN)
|
|
104
|
+
|
|
105
|
+
# The transition is automatically recorded
|
|
106
|
+
print(ctx._audit) # [(State.LOCKED, Event.COIN)]
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Testing
|
|
110
|
+
|
|
111
|
+
The project includes a comprehensive suite of unit and integration tests.
|
|
112
|
+
|
|
113
|
+
### Run All Tests
|
|
114
|
+
```bash
|
|
115
|
+
uv run pytest tests/
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Run with Coverage
|
|
119
|
+
```bash
|
|
120
|
+
uv run pytest --cov=src tests/
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The current implementation maintains **100% code coverage** across the core logic and integration workflows.
|
|
124
|
+
|
|
125
|
+
## Project Structure
|
|
126
|
+
|
|
127
|
+
- `src/semantic_state_machine/`: Core implementation.
|
|
128
|
+
- `tests/unit/`: Focused unit tests for individual components.
|
|
129
|
+
- `tests/integration/`: End-to-end workflow simulations.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# State Machine
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe Python state machine implementation using modern Python 3.13+ features like PEP 695 type parameters.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Type Safety**: Leverage Python's type system to ensure consistent states, events, and contexts.
|
|
8
|
+
- **Decorator Support**: Easily define transitions using the `@sm.transition` decorator.
|
|
9
|
+
- **Auditing**: Built-in support for recording transition history via `AuditedStateMachine` and `AuditContext`.
|
|
10
|
+
- **Flexible Context**: Pass a custom context object to transition actions to maintain state outside the machine.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
This project uses `uv` for dependency management. To get started, ensure you have `uv` installed and run:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
uv sync
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
You can install the package via pip:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install semantic-state-machine
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
### Basic State Machine
|
|
29
|
+
|
|
30
|
+
Define your states and events as `Enum`s and create a context class.
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from enum import Enum
|
|
34
|
+
from semantic_state_machine import StateMachine
|
|
35
|
+
|
|
36
|
+
class State(Enum):
|
|
37
|
+
LOCKED = 1
|
|
38
|
+
UNLOCKED = 2
|
|
39
|
+
|
|
40
|
+
class Event(Enum):
|
|
41
|
+
PUSH = 1
|
|
42
|
+
COIN = 2
|
|
43
|
+
|
|
44
|
+
class TurnstileContext:
|
|
45
|
+
def __init__(self):
|
|
46
|
+
self.total_coins = 0
|
|
47
|
+
|
|
48
|
+
sm = StateMachine[State, Event, TurnstileContext]()
|
|
49
|
+
|
|
50
|
+
@sm.transition(State.LOCKED, Event.COIN, State.UNLOCKED)
|
|
51
|
+
def insert_coin(ctx: TurnstileContext):
|
|
52
|
+
ctx.total_coins += 1
|
|
53
|
+
print("Coin inserted. Turnstile unlocked.")
|
|
54
|
+
|
|
55
|
+
@sm.transition(State.UNLOCKED, Event.PUSH, State.LOCKED)
|
|
56
|
+
def push_turnstile(ctx: TurnstileContext):
|
|
57
|
+
print("Turnstile pushed. Turnstile locked.")
|
|
58
|
+
|
|
59
|
+
# Usage
|
|
60
|
+
ctx = TurnstileContext()
|
|
61
|
+
current_state = State.LOCKED
|
|
62
|
+
current_state = sm.handle_transition(ctx, current_state, Event.COIN)
|
|
63
|
+
# current_state is now State.UNLOCKED
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Audited State Machine
|
|
67
|
+
|
|
68
|
+
Use `AuditedStateMachine` and `AuditContext` to automatically track the history of transitions.
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from semantic_state_machine import AuditedStateMachine, AuditContext
|
|
72
|
+
|
|
73
|
+
class MyContext(AuditContext[State, Event]):
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
sm = AuditedStateMachine[State, Event, MyContext]()
|
|
77
|
+
|
|
78
|
+
# Transitions are defined the same way
|
|
79
|
+
@sm.transition(State.LOCKED, Event.COIN, State.UNLOCKED)
|
|
80
|
+
def insert_coin(ctx: MyContext):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
ctx = MyContext()
|
|
84
|
+
sm.handle_transition(ctx, State.LOCKED, Event.COIN)
|
|
85
|
+
|
|
86
|
+
# The transition is automatically recorded
|
|
87
|
+
print(ctx._audit) # [(State.LOCKED, Event.COIN)]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Testing
|
|
91
|
+
|
|
92
|
+
The project includes a comprehensive suite of unit and integration tests.
|
|
93
|
+
|
|
94
|
+
### Run All Tests
|
|
95
|
+
```bash
|
|
96
|
+
uv run pytest tests/
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Run with Coverage
|
|
100
|
+
```bash
|
|
101
|
+
uv run pytest --cov=src tests/
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The current implementation maintains **100% code coverage** across the core logic and integration workflows.
|
|
105
|
+
|
|
106
|
+
## Project Structure
|
|
107
|
+
|
|
108
|
+
- `src/semantic_state_machine/`: Core implementation.
|
|
109
|
+
- `tests/unit/`: Focused unit tests for individual components.
|
|
110
|
+
- `tests/integration/`: End-to-end workflow simulations.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "semantic-state-machine"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A lightweight, type-safe Python state machine implementation using modern Python 3.13+ features."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Richard West", email = "dopplereffect.us@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = []
|
|
11
|
+
keywords = ["state-machine", "finite-state-machine", "type-safety", "audit"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
18
|
+
]
|
|
19
|
+
license = { text = "MIT" }
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/rjdw/semantic-state-machine"
|
|
23
|
+
Repository = "https://github.com/rjdw/semantic-state-machine"
|
|
24
|
+
Issues = "https://github.com/rjdw/semantic-state-machine/issues"
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[dependency-groups]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=9.0.3",
|
|
33
|
+
"pytest-cov>=7.1.0",
|
|
34
|
+
"pytest-gitignore>=1.3",
|
|
35
|
+
"pytest-html-plus>=0.5.2",
|
|
36
|
+
"pytest-isort>=4.0.0",
|
|
37
|
+
"pytest-randomly>=4.0.1",
|
|
38
|
+
"pytest-timeout>=2.4.0",
|
|
39
|
+
"pytest-xdist>=3.8.0",
|
|
40
|
+
"ruff>=0.15.10",
|
|
41
|
+
"ty>=0.0.29",
|
|
42
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from .state_machine import (
|
|
2
|
+
Action,
|
|
3
|
+
AuditContext,
|
|
4
|
+
AuditedStateMachine,
|
|
5
|
+
InvalidTransition,
|
|
6
|
+
StateMachine,
|
|
7
|
+
Transition,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"Action",
|
|
12
|
+
"AuditContext",
|
|
13
|
+
"AuditedStateMachine",
|
|
14
|
+
"InvalidTransition",
|
|
15
|
+
"StateMachine",
|
|
16
|
+
"Transition",
|
|
17
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Callable, Iterable, cast
|
|
4
|
+
|
|
5
|
+
type Action[C] = Callable[[C], None]
|
|
6
|
+
"""A callable that performs an action on a context object."""
|
|
7
|
+
|
|
8
|
+
type Transition[S: Enum, E: Enum, C] = dict[tuple[S, E], tuple[S, Action[C]]]
|
|
9
|
+
"""A mapping of (state, event) to (target_state, action)."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class AuditContext[S: Enum, E: Enum]:
|
|
14
|
+
"""A context class that records the history of state transitions.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
S: The Enum type representing the states.
|
|
18
|
+
E: The Enum type representing the events.
|
|
19
|
+
|
|
20
|
+
Notes:
|
|
21
|
+
Architectural Intent: Provides a standardized mixin for state machine
|
|
22
|
+
contexts that require auditing capabilities. It is designed to be
|
|
23
|
+
inherited by user-defined context classes.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
_audit: list[tuple[S, E]] = field(default_factory=list)
|
|
27
|
+
|
|
28
|
+
def record_transition(self, from_state: S, event: E) -> None:
|
|
29
|
+
"""Records a transition from a given state triggered by an event.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
from_state: The state the machine is transitioning from.
|
|
33
|
+
event: The event that triggered the transition.
|
|
34
|
+
"""
|
|
35
|
+
self._audit.append((from_state, event))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvalidTransition(Exception):
|
|
39
|
+
"""Raised when a transition is not defined for a given state and event."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class StateMachine[S: Enum, E: Enum, C]:
|
|
46
|
+
"""A lightweight, type-safe state machine.
|
|
47
|
+
|
|
48
|
+
The state machine is stateless; the current state must be managed externally
|
|
49
|
+
and passed to the `handle_transition` method.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
S: The Enum type representing the states.
|
|
53
|
+
E: The Enum type representing the events.
|
|
54
|
+
C: The type of the context object passed to actions.
|
|
55
|
+
|
|
56
|
+
Notes:
|
|
57
|
+
Architectural Intent: Leverages PEP 695 type parameters to ensure
|
|
58
|
+
strict type safety across states, events, and context objects without
|
|
59
|
+
sacrificing flexibility.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
_transitions: Transition[S, E, C] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
def add_transition(
|
|
65
|
+
self, from_state: S, event: E, to_state: S, func: Action[C]
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Manually registers a transition between states.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
from_state: The starting state for the transition.
|
|
71
|
+
event: The event that triggers the transition.
|
|
72
|
+
to_state: The target state after the transition.
|
|
73
|
+
func: The action to execute during the transition.
|
|
74
|
+
"""
|
|
75
|
+
self._transitions[(from_state, event)] = (to_state, func)
|
|
76
|
+
|
|
77
|
+
def handle_transition(self, ctx: C, from_state: S, event: E) -> S:
|
|
78
|
+
"""Executes a transition and returns the new state.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
ctx: The context object to pass to the transition's action.
|
|
82
|
+
from_state: The current state of the machine.
|
|
83
|
+
event: The event to process.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
S: The new state of the machine.
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
InvalidTransition: If the transition is not defined for the
|
|
90
|
+
given state and event.
|
|
91
|
+
"""
|
|
92
|
+
to_state, action = self._next_transition(from_state, event)
|
|
93
|
+
action(ctx)
|
|
94
|
+
return to_state
|
|
95
|
+
|
|
96
|
+
def _next_transition(self, from_state: S, event: E) -> tuple[S, Action[C]]:
|
|
97
|
+
"""Internal helper to retrieve the next transition.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
from_state: The current state.
|
|
101
|
+
event: The event.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
tuple[S, Action[C]]: A tuple of (target_state, action).
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
InvalidTransition: If the transition key is missing.
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
return self._transitions[(from_state, event)]
|
|
111
|
+
except KeyError as e:
|
|
112
|
+
raise InvalidTransition(
|
|
113
|
+
f"Cannot {event.name} when {from_state.name}"
|
|
114
|
+
) from e
|
|
115
|
+
|
|
116
|
+
def transition(
|
|
117
|
+
self, from_state: S | Iterable[S], event: E, to_state: S
|
|
118
|
+
) -> Callable[[Action[C]], Action[C]]:
|
|
119
|
+
"""A decorator to register a transition for a function.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
from_state: A single state or an iterable of states that can
|
|
123
|
+
trigger this transition.
|
|
124
|
+
event: The event that triggers the transition.
|
|
125
|
+
to_state: The target state after the transition.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Callable[[Action[C]], Action[C]]: The decorator function.
|
|
129
|
+
|
|
130
|
+
Notes:
|
|
131
|
+
Architectural Intent: Allows for a declarative way to map
|
|
132
|
+
multiple source states to a single target state and action.
|
|
133
|
+
"""
|
|
134
|
+
states: list[S]
|
|
135
|
+
if isinstance(from_state, Enum):
|
|
136
|
+
states = [cast(S, from_state)]
|
|
137
|
+
else:
|
|
138
|
+
states = list(cast(Iterable[S], from_state))
|
|
139
|
+
|
|
140
|
+
def decorator(func: Action[C]) -> Action[C]:
|
|
141
|
+
for s in states:
|
|
142
|
+
self.add_transition(s, event, to_state, func)
|
|
143
|
+
return func
|
|
144
|
+
|
|
145
|
+
return decorator
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass
|
|
149
|
+
class AuditedStateMachine[S: Enum, E: Enum, C: AuditContext](StateMachine[S, E, C]):
|
|
150
|
+
"""A state machine that automatically records transitions in the context.
|
|
151
|
+
|
|
152
|
+
Requires a context that inherits from `AuditContext`.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
S: The Enum type representing the states.
|
|
156
|
+
E: The Enum type representing the events.
|
|
157
|
+
C: A context type that implements `AuditContext`.
|
|
158
|
+
|
|
159
|
+
Notes:
|
|
160
|
+
Architectural Intent: Simplifies auditing by automatically calling
|
|
161
|
+
`record_transition` on the context before executing the standard
|
|
162
|
+
transition logic.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def handle_transition(self, ctx: C, from_state: S, event: E) -> S:
|
|
166
|
+
"""Records the transition and then executes it.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
ctx: The audit context.
|
|
170
|
+
from_state: The current state.
|
|
171
|
+
event: The event to process.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
S: The new state.
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
InvalidTransition: If the transition is not defined.
|
|
178
|
+
"""
|
|
179
|
+
ctx.record_transition(from_state, event)
|
|
180
|
+
return super().handle_transition(ctx, from_state, event)
|