Skip to content

Instantly share code, notes, and snippets.

@HennyH
Last active June 7, 2021 08:56
Show Gist options
  • Save HennyH/b86c29e7bc1d77b965f357b80f09d714 to your computer and use it in GitHub Desktop.
Save HennyH/b86c29e7bc1d77b965f357b80f09d714 to your computer and use it in GitHub Desktop.
Simple troubleshooter program.
[pc_wall]
root = true
text = Is the switch at the wall to which the computer power cable is conencted turned on?
if_no = switch_on (object:wall switch) (next:pc_psu)
if_yes = pc_psu
if_is_laptop = pc_light
if_they_dont_know = switch_on ()
[switch_on]
text = Have you turned ${object:the switch} on?
if_no = switch_on (object:${object}) (next:${next})
if_yes = ${next:end}
[pc_psu]
text = Is the switch on the PSU (probably at the back on the computer) turned on (the 'I' symbol down)?
if_no = switch_on (object:PSU switch) (next:pc_light)
if_yes = pc_light
[pc_light]
text = "Is the lighting up?"
[end]
text = "I hope your computer works now!"
import argparse
import configparser
import re
import sys
from typing import Dict, Iterable, Optional, Tuple
def ask_choice(prompt, choices):
if not choices:
return None
lowered_choices = [o.lower().strip() for o in choices]
while True:
print(f":: {prompt}")
for i, option in enumerate(choices):
print(f" {i} {option}")
answer = input("> ").strip().lower()
if answer in lowered_choices:
return answer
try:
if 0 <= int(answer) < len(choices):
return choices[int(answer)]
except:
pass
def perform_subsitutions(text, variables):
"""Subsititues any occurances of ${NAME[:DEFAULT]} with the value from dictionary."""
def evaluate_match(match):
captures = match.groupdict()
default = captures.get("default") or ""
variable_name = captures.get("name") or ""
if variable_name not in variables:
return default
return variables[variable_name]
return re.sub(r"\$\{(?P<name>\w*):?(?P<default>.*?)\}",
evaluate_match,
text)
def parse_node_invocation(invocation: str) -> Tuple[str, Dict[str, str]]:
"""Parse a node invocation like `switch_on [(param:value), ...]` into node_id and the variable dict."""
node_id_and_maybe_variables_text = re.split(
r"\s+", invocation.strip(), maxsplit=1)
node_id = node_id_and_maybe_variables_text[0]
variables = {}
if len(node_id_and_maybe_variables_text) == 2:
variables_text = node_id_and_maybe_variables_text[1]
for variable_text in re.finditer(r"\((?P<variable>[^()]+)\)", variables_text):
name, value = variable_text.group("variable").split(":")
variables[name] = value
return (node_id, variables)
class Node():
"""Represents a node in the decision tree."""
def __init__(self, node_id, text: str, root: Optional[bool] = False, **kwargs):
self.node_id = node_id
self.text = text
self.root = False if root is None else bool(root)
self.choice_to_node_invocation = {}
for name, value in kwargs.items():
branch_match = re.match(r"if_(?P<choice>\w+)", name, re.I)
if not branch_match:
continue
choice = branch_match.groupdict().get("choice")
if not choice:
raise ValueError(
"A choice text cannot be empty, if_<x> must have a non-empty x")
self.choice_to_node_invocation[choice] = value
def run(self, variables: Dict[str, str]) -> Optional[Tuple[str, Dict[str, str]]]:
"""Run the node, returning the next node and invocation variables if any."""
choices = []
for choice, _ in self.choice_to_node_invocation.items():
choices.append(choice)
choices.append("exit")
prompt = perform_subsitutions(self.text, variables)
choice = ask_choice(prompt, choices=choices)
if choice is None or choice == "exit":
return None
node_invocation = self.choice_to_node_invocation[choice]
node_invocation = perform_subsitutions(node_invocation, variables)
return parse_node_invocation(node_invocation)
def __str__(self):
return f"Node(id={self.node_id}, text={self.text}, root={self.root}, choices={self.choice_to_node_invocation})"
class Scenario():
"""Represents a collection of nodes for a troubleshooting scenario."""
def __init__(self, config_file_obj: Iterable[str]):
config_parser = configparser.ConfigParser()
config_parser.read_file(config_file_obj)
self.node_id_to_node = {}
for node_id, settings in config_parser.items():
if node_id == configparser.DEFAULTSECT:
continue
if settings.get("text") is None:
raise ValueError("Each node must contain a text setting")
self.node_id_to_node[node_id] = Node(node_id, **settings)
def get_node(self, node_id: str) -> Node:
if not re.match(r"\w+", node_id, re.I):
raise ValueError(f"Invalid node id {node_id}")
return self.node_id_to_node.get(node_id)
def get_root_node_id(self) -> str:
for id, node in self.node_id_to_node.items():
if node.root == True:
return id
return None
class Troubleshooter():
"""Create a troubleshooter which uses the given scenario to help users."""
def __init__(self, scenario: Scenario):
self.scenario = scenario
def get_help(self):
node_id, variables = self.scenario.get_root_node_id(), {}
if not node_id:
raise Exception("No root node in scenario")
while node_id is not None:
node = self.scenario.get_node(node_id)
if node is None:
raise Exception(f"Node with id {node_id} could not be found")
next = node.run(variables)
if next is None:
return "OK"
node_id, variables = next
def main(argv=None):
argv = argv or sys.argv[1:]
parser = argparse.ArgumentParser("Troubleshooter")
parser.add_argument("--scenario",
type=argparse.FileType("r"),
required=True)
result = parser.parse_args(argv)
troubleshooter = Troubleshooter(Scenario(result.scenario))
troubleshooter.get_help()
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment