Add python/state-cli.md
This commit is contained in:
parent
2a0ccb44d8
commit
e645bb52f2
191
python/state-cli.md
Normal file
191
python/state-cli.md
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
Implementation of a command line interface that provides different commands
|
||||||
|
based on the state of a state machine.
|
||||||
|
|
||||||
|
```
|
||||||
|
import prompt_toolkit
|
||||||
|
from prompt_toolkit.styles import Style
|
||||||
|
from prompt_toolkit.patch_stdout import StdoutProxy, patch_stdout
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# StateCli
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class StateCommandBase:
|
||||||
|
"""Base class for state-specific commands."""
|
||||||
|
|
||||||
|
def __init__(self, cli, state_machine):
|
||||||
|
self.cli = cli
|
||||||
|
self.state_machine = state_machine
|
||||||
|
|
||||||
|
|
||||||
|
class StateCli:
|
||||||
|
"""
|
||||||
|
Implementation of a CLI working in conjuction with a state machine.
|
||||||
|
Depending on the state, the CLI provides different commands.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, state_machine, command_map):
|
||||||
|
self.state_machine = state_machine
|
||||||
|
|
||||||
|
self.state_commands = {
|
||||||
|
key: value(self, state_machine) for key, value in command_map.items()
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_prompt()
|
||||||
|
|
||||||
|
self._prompt_style = Style.from_dict(
|
||||||
|
{
|
||||||
|
"prompt": "ansiblue bold",
|
||||||
|
"input": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.session = prompt_toolkit.PromptSession()
|
||||||
|
|
||||||
|
def update_prompt(self):
|
||||||
|
self.prompt = f"{self.state_machine.current_state}> "
|
||||||
|
|
||||||
|
def _process_command(self, command, args):
|
||||||
|
current_state = self.state_machine.current_state
|
||||||
|
if current_state not in self.state_commands:
|
||||||
|
print(f"No commands defined for state {current_state}")
|
||||||
|
return
|
||||||
|
|
||||||
|
state_cmd_obj = self.state_commands[current_state]
|
||||||
|
if not hasattr(state_cmd_obj, command):
|
||||||
|
print(f"Unknown command: '{command}'")
|
||||||
|
self._do_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
getattr(state_cmd_obj, command)(" ".join(args))
|
||||||
|
|
||||||
|
def cmdloop(self):
|
||||||
|
with patch_stdout():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
user_input = self.session.prompt(
|
||||||
|
self.prompt, style=self._prompt_style
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = user_input.split()
|
||||||
|
command, args = parts[0], parts[1:]
|
||||||
|
|
||||||
|
if command == "help":
|
||||||
|
self._do_help()
|
||||||
|
continue
|
||||||
|
elif command == "exit":
|
||||||
|
break
|
||||||
|
|
||||||
|
self._process_command(command, args)
|
||||||
|
except KeyboardInterrupt: # ctrl+c
|
||||||
|
break
|
||||||
|
except EOFError: # ctrl+d
|
||||||
|
break
|
||||||
|
|
||||||
|
def _do_help(self):
|
||||||
|
current_state = self.state_machine.current_state
|
||||||
|
print("=" * 50)
|
||||||
|
print("Available commands in any state")
|
||||||
|
print(f" {'help':15} - Show this help")
|
||||||
|
print(f" {'exit':15} - Exit the application")
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
print(f"Available commands in state {current_state}")
|
||||||
|
|
||||||
|
if current_state not in self.state_commands:
|
||||||
|
print(f" No commands defined for state {current_state}")
|
||||||
|
print("=" * 50)
|
||||||
|
return
|
||||||
|
|
||||||
|
state_cmd_obj = self.state_commands[current_state]
|
||||||
|
commands = [
|
||||||
|
name
|
||||||
|
for name in dir(state_cmd_obj)
|
||||||
|
if not name.startswith("_") and callable(getattr(state_cmd_obj, name))
|
||||||
|
]
|
||||||
|
|
||||||
|
if not commands:
|
||||||
|
print(" No commands available")
|
||||||
|
print("=" * 50)
|
||||||
|
return
|
||||||
|
|
||||||
|
for cmd in sorted(commands):
|
||||||
|
method = getattr(state_cmd_obj, cmd)
|
||||||
|
if method.__doc__:
|
||||||
|
doc = method.__doc__.strip()
|
||||||
|
first_line = doc.split("\n")[0]
|
||||||
|
print(f" {cmd:15} - {first_line}")
|
||||||
|
else:
|
||||||
|
print(f" {cmd:15} - No documentation available")
|
||||||
|
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# StateMachine
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class StateMachine:
|
||||||
|
def __init__(self):
|
||||||
|
self.current_state = "idle"
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
self.current_state = "running"
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.current_state = "idle"
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Usage
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
class IdleCommands(StateCommandBase):
|
||||||
|
def start(self, args):
|
||||||
|
"""Start the machine"""
|
||||||
|
print("Starting the machine")
|
||||||
|
self.state_machine.start()
|
||||||
|
self.cli.update_prompt()
|
||||||
|
|
||||||
|
|
||||||
|
class RunningCommands(StateCommandBase):
|
||||||
|
def _stop(self, args):
|
||||||
|
"""Stop the machine"""
|
||||||
|
print("Stopping the the machine")
|
||||||
|
self.state_machine.stop()
|
||||||
|
self.cli.update_prompt()
|
||||||
|
|
||||||
|
|
||||||
|
def background_task():
|
||||||
|
while True:
|
||||||
|
time.sleep(2)
|
||||||
|
logging.info("This is a log message from some background task")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
sm = StateMachine()
|
||||||
|
cli = StateCli(sm, {"idle": IdleCommands, "running": RunningCommands})
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(asctime)s %(levelname)s %(message)s",
|
||||||
|
datefmt="%H:%M:%S",
|
||||||
|
level=logging.INFO,
|
||||||
|
stream=StdoutProxy(),
|
||||||
|
)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=background_task, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
cli.cmdloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user