0
# Events and Transitions
1
2
Event handling and state transition management including event triggers, transition definitions with conditions, guards and validators, and transition lifecycle callbacks.
3
4
## Capabilities
5
6
### Event Class
7
8
Events trigger state transitions and carry data to callbacks and actions.
9
10
```python { .api }
11
class Event(str):
12
"""
13
An event triggers transitions between states.
14
15
Events inherit from str and can be used as string identifiers.
16
They carry information about triggers and associated transitions.
17
18
Parameters:
19
- transitions: Event transitions (string, TransitionList, or None)
20
- id: Event identifier (optional, generated if not provided)
21
- name: Event name identifier (optional, derived from id)
22
"""
23
def __new__(cls, transitions=None, id=None, name=None, _sm=None): ...
24
25
@property
26
def id(self) -> str:
27
"""Get the event identifier."""
28
29
@property
30
def name(self) -> str:
31
"""Get the human-readable event name."""
32
33
@property
34
def transitions(self) -> TransitionList:
35
"""Get transitions associated with this event."""
36
37
def __call__(self, *args, **kwargs):
38
"""Trigger the event (for bound events)."""
39
40
def split(self, sep=None, maxsplit=-1):
41
"""Split event into multiple events (string method)."""
42
43
def is_same_event(self, event=None):
44
"""Check if this event matches the given event."""
45
46
def add_callback(self, callback, group=None, priority=None):
47
"""Add callback to event execution."""
48
```
49
50
### BoundEvent Class
51
52
Runtime event instances bound to specific state machine instances.
53
54
```python { .api }
55
class BoundEvent(Event):
56
"""
57
Event bound to a specific state machine instance.
58
59
Provides callable interface for triggering events directly.
60
"""
61
def __call__(self, *args, **kwargs):
62
"""Call the event to trigger associated transitions."""
63
64
async def __call__(self, *args, **kwargs):
65
"""Async version for async state machines."""
66
```
67
68
### Transition Class
69
70
Defines connections between states with optional conditions, guards, and actions.
71
72
```python { .api }
73
class Transition:
74
"""
75
Represents a state transition with source, target, and conditions.
76
77
Parameters:
78
- source: Source State object
79
- target: Target State object
80
- event: Event trigger(s) - string, list of strings, or space-separated string
81
- internal: Internal transition flag (doesn't trigger entry/exit actions)
82
- validators: Validation callbacks invoked before transition
83
- cond: Condition callbacks that must evaluate to True
84
- unless: Condition callbacks that must evaluate to False
85
- on: Action callbacks executed during transition
86
- before: Callbacks executed before transition
87
- after: Callbacks executed after transition
88
"""
89
def __init__(self, source: State, target: State, event=None, internal: bool = False, validators=None, cond=None, unless=None, on=None, before=None, after=None): ...
90
91
@property
92
def source(self) -> State:
93
"""Get the source state."""
94
95
@property
96
def target(self) -> State:
97
"""Get the target state."""
98
99
@property
100
def event(self) -> str:
101
"""Get the event identifier."""
102
103
@property
104
def internal(self) -> bool:
105
"""Check if transition is internal."""
106
107
def match(self, event: str) -> bool:
108
"""Check if transition matches given event."""
109
110
def add_event(self, event):
111
"""Add event to this transition."""
112
113
def execute(self, machine, event_data):
114
"""Execute the transition."""
115
```
116
117
### TransitionList Class
118
119
Collection of transitions with callback support and fluent API.
120
121
```python { .api }
122
class TransitionList:
123
"""
124
Collection of transitions supporting callbacks and fluent operations.
125
126
Allows chaining transitions and adding shared callbacks.
127
"""
128
def __init__(self, transitions=None): ...
129
130
def add_transitions(self, transitions):
131
"""Add transitions to the collection."""
132
133
def add_event(self, event):
134
"""Add event to all transitions in the collection."""
135
136
def unique_events(self):
137
"""Get unique events across all transitions."""
138
139
def __or__(self, other) -> TransitionList:
140
"""Chain transitions using | operator."""
141
142
def __getitem__(self, index):
143
"""Get transition by index."""
144
145
def __len__(self) -> int:
146
"""Get number of transitions in the list."""
147
148
def cond(self, *callbacks) -> TransitionList:
149
"""Add condition callbacks to all transitions."""
150
151
def unless(self, *callbacks) -> TransitionList:
152
"""Add unless callbacks to all transitions."""
153
154
def on(self, *callbacks) -> TransitionList:
155
"""Add action callbacks to all transitions."""
156
157
def before(self, *callbacks) -> TransitionList:
158
"""Add before callbacks to all transitions."""
159
160
def after(self, *callbacks) -> TransitionList:
161
"""Add after callbacks to all transitions."""
162
```
163
164
## Usage Examples
165
166
### Basic Event and Transition Definition
167
168
```python
169
from statemachine import StateMachine, State, Event
170
171
class OrderMachine(StateMachine):
172
# Define states
173
pending = State(initial=True)
174
paid = State()
175
shipped = State()
176
delivered = State(final=True)
177
cancelled = State(final=True)
178
179
# Define events and transitions
180
pay = pending.to(paid)
181
ship = paid.to(shipped)
182
deliver = shipped.to(delivered)
183
cancel = (
184
pending.to(cancelled)
185
| paid.to(cancelled)
186
| shipped.to(cancelled)
187
)
188
189
# Usage
190
order = OrderMachine()
191
order.send("pay") # Trigger pay event
192
order.pay() # Alternative direct call
193
print(order.current_state.id) # "paid"
194
```
195
196
### Conditional Transitions with Guards
197
198
```python
199
class ATMMachine(StateMachine):
200
idle = State(initial=True)
201
card_inserted = State()
202
pin_entered = State()
203
authenticated = State()
204
205
insert_card = idle.to(card_inserted)
206
enter_pin = card_inserted.to(pin_entered)
207
208
# Conditional transition with guard
209
authenticate = pin_entered.to(authenticated).cond("valid_pin")
210
reject = pin_entered.to(idle).unless("valid_pin")
211
212
def valid_pin(self, pin: str = None):
213
"""Guard function - return True to allow transition."""
214
return pin == "1234"
215
216
# Usage with event data
217
atm = ATMMachine()
218
atm.send("insert_card")
219
atm.send("enter_pin")
220
atm.send("authenticate", pin="1234") # Will succeed
221
```
222
223
### Complex Transition Chains
224
225
```python
226
class WorkflowMachine(StateMachine):
227
start = State(initial=True)
228
step1 = State()
229
step2 = State()
230
step3 = State()
231
completed = State(final=True)
232
error = State(final=True)
233
234
# Chain multiple transitions with shared conditions
235
process = (
236
start.to(step1)
237
| step1.to(step2).cond("step1_valid")
238
| step2.to(step3).cond("step2_valid")
239
| step3.to(completed).cond("step3_valid")
240
).on("log_progress").before("validate_step")
241
242
# Error transitions
243
handle_error = (
244
step1.to(error)
245
| step2.to(error)
246
| step3.to(error)
247
).unless("step_valid")
248
249
def validate_step(self, **kwargs):
250
print("Validating step...")
251
252
def log_progress(self, source: State, target: State, **kwargs):
253
print(f"Moving from {source.id} to {target.id}")
254
255
def step1_valid(self, data=None):
256
return data and data.get("step1_complete", False)
257
258
def step2_valid(self, data=None):
259
return data and data.get("step2_complete", False)
260
261
def step3_valid(self, data=None):
262
return data and data.get("step3_complete", False)
263
264
def step_valid(self, **kwargs):
265
return True # Simplified validation
266
```
267
268
### Internal Transitions
269
270
```python
271
class MediaPlayerMachine(StateMachine):
272
stopped = State(initial=True)
273
playing = State()
274
paused = State()
275
276
play = stopped.to(playing) | paused.to(playing)
277
pause = playing.to(paused)
278
stop = playing.to(stopped) | paused.to(stopped)
279
280
# Internal transition - doesn't trigger entry/exit actions
281
seek = playing.to(playing, internal=True).on("update_position")
282
283
def on_enter_playing(self):
284
print("Starting playback")
285
286
def on_exit_playing(self):
287
print("Stopping playback")
288
289
def update_position(self, position: int):
290
print(f"Seeking to position: {position}")
291
292
# Usage
293
player = MediaPlayerMachine()
294
player.send("play") # Prints: "Starting playback"
295
player.send("seek", position=30) # Prints: "Seeking to position: 30"
296
# No entry/exit messages due to internal=True
297
```
298
299
### Event Data and Parameter Injection
300
301
```python
302
class NotificationMachine(StateMachine):
303
draft = State(initial=True)
304
scheduled = State()
305
sent = State(final=True)
306
307
schedule = draft.to(scheduled)
308
send = scheduled.to(sent)
309
310
def before_schedule(self, event: str, source: State, target: State,
311
when: str = None, recipients: list = None):
312
"""Parameters are automatically injected from event data."""
313
print(f"Scheduling {event} from {source.id} to {target.id}")
314
if when:
315
print(f"Scheduled for: {when}")
316
if recipients:
317
print(f"Recipients: {', '.join(recipients)}")
318
319
def on_send(self, message: str = "Default message"):
320
print(f"Sending: {message}")
321
322
# Usage with event parameters
323
notification = NotificationMachine()
324
notification.send(
325
"schedule",
326
when="2023-12-25 09:00",
327
recipients=["user1@example.com", "user2@example.com"]
328
)
329
notification.send("send", message="Happy Holidays!")
330
```
331
332
### Async Event Handling
333
334
```python
335
import asyncio
336
from statemachine import StateMachine, State
337
338
class AsyncProcessMachine(StateMachine):
339
idle = State(initial=True)
340
processing = State()
341
completed = State(final=True)
342
343
start = idle.to(processing)
344
finish = processing.to(completed)
345
346
async def on_enter_processing(self, task_data=None):
347
"""Async action handler."""
348
print("Starting async processing...")
349
if task_data:
350
await self.process_data(task_data)
351
print("Processing completed")
352
353
async def process_data(self, data):
354
"""Simulate async work."""
355
await asyncio.sleep(2)
356
print(f"Processed: {data}")
357
358
# Usage
359
async def main():
360
machine = AsyncProcessMachine()
361
await machine.send_async("start", task_data="important_data")
362
await machine.send_async("finish")
363
364
asyncio.run(main())
365
```