A framework for creating and managing finite state machines (FSMs).
Project description
Enoki - A python state machine framework
Enoki is a finite state machine library for asynchronous event based systems.
Features
- State Lifecycle Management: Complete control over state entry, execution, and exit
- Retry and Timeout Support: Built-in mechanisms for handling transient failures and preventing deadlocks
- Pushdown Automata: State stack support for complex state hierarchies
- Message-Driven Architecture: Event-driven state transitions with message queues
- Shared State Management: Pass data between states seamlessly
- Error Handling: Comprehensive exception handling with custom error states
- Visualization: Generate Graphviz and Mermaid diagrams of state transitions
- Non-blocking Execution: Support for both blocking and non-blocking state machine execution
Quick Start
Basic Example
import enoki
from enoki import State
class Ping(State):
def on_state(self, shared_state):
print("Ping!")
return Pong
class Pong(State):
def on_state(self, shared_state):
print("Pong!")
return Ping
class ErrorState(State):
def on_state(self, shared_state):
print("Error occurred")
# Create and run the state machine
fsm = enoki.StateMachine(
initial_state=Ping,
final_state=enoki.DefaultStates.End,
default_error_state=ErrorState
)
# Single step execution
fsm.tick() # Prints "Ping!" and transitions to Pong
Core Concepts
States
States are the fundamental building blocks of your FSM. Each state inherits from the State base class and implements lifecycle methods:
on_enter(shared_state): Called when entering the stateon_state(shared_state): Main state logic (required)on_leave(shared_state): Called when leaving the stateon_fail(shared_state): Called when retry limit is exceededon_timeout(shared_state): Called when state timeout occurs
class ExampleState(State):
TIMEOUT = 30 # Optional: timeout in seconds
RETRIES = 3 # Optional: number of retries before failure
def on_enter(self, shared_state):
print("Entering state")
def on_state(self, shared_state):
# Your state logic here
if some_condition:
return NextState # Transition to NextState
elif should_retry:
return ExampleState # Retry current state (decrements retry counter)
else:
return # Stay in current state (wait)
def on_leave(self, shared_state):
print("Leaving state")
Transitions
States can return different values to control transitions:
NextState: Transition to a different statetype(self)or the constructor for the current state: Retry the current state immediately (triggers retry counter)Repeat: Re-enter the same state from the beginning on the next tickPush(State1, State2, ...): Push states onto stack and transition to firstPop: Pop and transition to top state from stackNone: Stay in current state (wait for next message)
Shared State
The SharedState object is passed to all state methods and contains:
fsm: Reference to the state machinecommon: Shared data object for passing information between statesmsg: Current message being processed
class DataProcessor(State):
def on_state(self, shared_state):
# Access shared data
shared_state.common.processed_count += 1
# Check current message
if shared_state.msg and shared_state.msg['type'] == 'data':
# Process the message
return ProcessingComplete
return None # Wait for more messages
Advanced Features
Message-Driven State Machines
Handle external events and messages:
import queue
def main():
msg_queue = queue.Queue()
fsm = enoki.StateMachine(
initial_state=WaitingState,
final_state=enoki.DefaultStates.End,
default_error_state=ErrorState,
msg_queue=msg_queue,
trap_fn=handle_unprocessed_messages # Optional message handler
)
# Send messages to the state machine
msg_queue.put({'type': 'start', 'data': 'hello'})
# Process messages
while not fsm.is_finished:
message = msg_queue.get()
fsm.tick(message)
State Stack (Pushdown Automata)
Use Push and Pop for hierarchical state management:
class MainMenu(State):
def on_state(self, shared_state):
if shared_state.msg['action'] == 'enter_submenu':
# Push current state and transition to submenu
return Push(SubMenu, SubMenuOption1, SubMenuOption2)
class SubMenu(State):
def on_state(self, shared_state):
if shared_state.msg['action'] == 'back':
# Return to previous state
return Pop
Retry and Timeout Handling
Enoki is designed for asynchronous systems where operations are initiated in on_enter and responses are handled in on_state:
class NetworkRequest(State):
TIMEOUT = 10 # 10 second timeout
RETRIES = 3 # Retry up to 3 times
def on_enter(self, shared_state):
# Initiate the async operation when entering the state
self.request_id = send_network_request_async()
print(f"Sent network request {self.request_id}")
def on_state(self, shared_state):
# Check for response messages
if shared_state.msg and shared_state.msg.get('request_id') == self.request_id:
if shared_state.msg['status'] == 'success':
return ProcessResponse
elif shared_state.msg['status'] == 'error':
return type(self) # Retry the request
# No matching response yet, keep waiting
return None
def on_timeout(self, shared_state):
print("Network request timed out")
return type(self) # Retry on timeout
def on_fail(self, shared_state):
print("Network request failed after all retries")
return ErrorState
Shared State Between States
class DataContainer:
def __init__(self):
self.user_data = {}
self.session_id = None
fsm = enoki.StateMachine(
initial_state=LoginState,
final_state=enoki.DefaultStates.End,
default_error_state=ErrorState,
common_data=DataContainer()
)
Message Filtering and Trapping
Enoki provides two mechanisms for handling messages that don't need to reach individual states:
Filter Function: Pre-processes messages before they reach states. If the filter returns True, the message is consumed and won't be passed to the current state. Useful for handling global messages like heartbeats or status updates.
Trap Function: Handles messages that states don't process (when on_state returns None). This catches "unhandled" messages and can be used for logging, error reporting, or default processing.
def message_filter(shared_state):
# Handle global messages that don't need state-specific processing
if shared_state.msg and shared_state.msg.get('type') == 'heartbeat':
shared_state.common.last_heartbeat = time.time()
return True # Message consumed, don't pass to state
return False # Let state handle the message
def message_trap(shared_state):
# Handle messages that states didn't process
msg = shared_state.msg
if msg:
print(f"Unhandled message in state {shared_state.fsm._current.name}: {msg}")
# Could log, raise exception, or take other action
fsm = enoki.StateMachine(
initial_state=StartState,
final_state=enoki.DefaultStates.End,
default_error_state=ErrorState,
filter_fn=message_filter,
trap_fn=message_trap
)
State Machine Configuration
The StateMachine constructor accepts several configuration options:
fsm = enoki.StateMachine(
initial_state=StartState, # Required: Starting state
final_state=EndState, # Required: Terminal state
default_error_state=ErrorState, # Required: Default error handler
msg_queue=queue.Queue(), # Optional: Message queue
filter_fn=message_filter, # Optional: Pre-filter messages
trap_fn=handle_unprocessed, # Optional: Handle unprocessed messages
on_error_fn=error_handler, # Optional: Global error handler
log_fn=print, # Optional: Logging function
transition_fn=log_transitions, # Optional: Transition callback
common_data=SharedData(), # Optional: Shared state object
dwell_states=[WaitState] # Optional: States that can wait indefinitely
)
Visualization
Generate visual representations of your state machine:
# Generate Mermaid flowchart
fsm.save_mermaid_flowchart('state_diagram.mmd')
# Generate Graphviz digraph
fsm.save_graphviz_digraph('state_diagram.dot')
Examples
The library includes several complete examples:
1. Free-Running State Machine (freerun.py)
Demonstrates a simple ping-pong state machine with retry logic that runs indefinitely.
2. Message-Driven State Machine (blocking.py)
Shows how to create a state machine that waits for specific messages, with shared state management and message trapping.
3. Event-Driven State Machine (event_driven.py)
Illustrates timeout handling and dwell states in an event-driven architecture.
Error Handling
Enoki provides comprehensive error handling:
StateRetryLimitError: Raised when a state exceeds its retry limitStateTimedOut: Raised when a state exceeds its timeout durationMissingOnStateHandler: Raised when a state lacks the requiredon_statemethodEmptyStateStackError: Raised when attempting to pop from an empty state stackBlockedInUntimedState: Raised when FSM is blocked in a state without timeout
Best Practices
- Always implement
on_state: This is the only required method for states - Use timeouts for blocking states: Prevent infinite waits with
TIMEOUT - Handle retries gracefully: Implement
on_failfor retry limit scenarios - Use shared state for data flow: Pass information between states via
common - Implement proper error states: Always provide meaningful error handling
- Leverage state stacks: Use
Push/Popfor hierarchical state management
Installation
# Copy enoki.py to your project directory
# No external dependencies required - uses only Python standard library
License
Enoki is released under the MIT License. See the LICENSE file for details.
Requirements
- Python >= 3.10
- GraphViz (Optional for state machine visualization)
Authors
Enoki was originally developed at Keyme under the name Mortise by Jeff Ciesielski and Lianne Lairmore for robotics control.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file enoki-1.2.0.tar.gz.
File metadata
- Download URL: enoki-1.2.0.tar.gz
- Upload date:
- Size: 5.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff9caa7478ae36c354dbb14f694d1e5bc1895a065709d8c9ff15f7ba3864c4f0
|
|
| MD5 |
af6f903a9b0bd50a92f0c00e90438ac9
|
|
| BLAKE2b-256 |
cbc897547f65f320e86d7fb4cd25cf77abb257457281ea7f6216900101f168e7
|
Provenance
The following attestation bundles were made for enoki-1.2.0.tar.gz:
Publisher:
python-publish.yml on Jeff-Ciesielski/enoki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
enoki-1.2.0.tar.gz -
Subject digest:
ff9caa7478ae36c354dbb14f694d1e5bc1895a065709d8c9ff15f7ba3864c4f0 - Sigstore transparency entry: 242284834
- Sigstore integration time:
-
Permalink:
Jeff-Ciesielski/enoki@36383063b1b436474b1b427fd4a08b15cafe57ac -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/Jeff-Ciesielski
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@36383063b1b436474b1b427fd4a08b15cafe57ac -
Trigger Event:
release
-
Statement type:
File details
Details for the file enoki-1.2.0-py3-none-any.whl.
File metadata
- Download URL: enoki-1.2.0-py3-none-any.whl
- Upload date:
- Size: 15.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ea6c80679c2247a34528d971cb4691cab5d86ebc467c837ec05345f48f61d40a
|
|
| MD5 |
69c1a36e590c6ec43f74cefea9cdfb92
|
|
| BLAKE2b-256 |
6b908ab1626e5c364068e0e502e61a9cfc22816c62bc234f0582b152ff8793d4
|
Provenance
The following attestation bundles were made for enoki-1.2.0-py3-none-any.whl:
Publisher:
python-publish.yml on Jeff-Ciesielski/enoki
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
enoki-1.2.0-py3-none-any.whl -
Subject digest:
ea6c80679c2247a34528d971cb4691cab5d86ebc467c837ec05345f48f61d40a - Sigstore transparency entry: 242284845
- Sigstore integration time:
-
Permalink:
Jeff-Ciesielski/enoki@36383063b1b436474b1b427fd4a08b15cafe57ac -
Branch / Tag:
refs/tags/v1.2.0 - Owner: https://github.com/Jeff-Ciesielski
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@36383063b1b436474b1b427fd4a08b15cafe57ac -
Trigger Event:
release
-
Statement type: