-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathinteractive_flow_creator.py
122 lines (104 loc) · 5.52 KB
/
interactive_flow_creator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
""" This module parses a json/yaml file that defines a flow of questions to fulfill the cookiecutter context"""
from typing import Dict, Optional, Tuple
import yaml
from samcli.commands.exceptions import UserException
from samcli.yamlhelper import parse_yaml_file
from .interactive_flow import InteractiveFlow
from .question import Question, QuestionFactory
class QuestionsNotFoundException(UserException):
pass
class QuestionsFailedParsingException(UserException):
pass
class InteractiveFlowCreator:
@staticmethod
def create_flow(flow_definition_path: str, extra_context: Optional[Dict] = None) -> InteractiveFlow:
"""
This method parses the given json/yaml file to create an InteractiveFLow. It expects the file to define
a list of questions. It parses the questions and add it to the flow in the same order they are defined
in the file, i.e. the default next-question of a given question will be the next one defined in the file,
while also respecting the question-defined _next_question map if provided.
Parameters:
----------
flow_definition_path: str
A path to a json/yaml file that defines the questions of the flow. the file is expected to be in the
following format:
{
"questions": [
{
"key": "key of the corresponding cookiecutter config",
"question": "the question to prompt to the user",
"kind": "the kind of the question, for example: confirm",
"isRequired": true/false,
# optional branching logic to jump to a particular question based on the answer. if not given
# will automatically go to next question
"nextQuestion": {
"True": "key of the question to jump to if the user answered 'Yes'",
"False": "key of the question to jump to if the user answered 'Yes'",
}
"default": "default_answer",
# the default value can also be loaded from cookiecutter context
# with a key path whose key path item can be loaded from cookiecutter as well.
"default": {
"keyPath": [
{
"valueOf": "key-of-another-question"
},
"pipeline_user"
]
}
# assuming the answer of "key-of-another-question" is "ABC"
# the default value will be load from cookiecutter context with key "['ABC', 'pipeline_user]"
},
...
]
}
extra_context: Dict
if the template contains variable($variableName) this parameter provides the values for those variables.
Returns: InteractiveFlow(questions={k1: q1, k2: q2, ...}, first_question_key="first question's key")
"""
questions, first_question_key = InteractiveFlowCreator._load_questions(flow_definition_path, extra_context)
return InteractiveFlow(questions=questions, first_question_key=first_question_key)
@staticmethod
def _load_questions(
flow_definition_path: str, extra_context: Optional[Dict] = None
) -> Tuple[Dict[str, Question], str]:
previous_question: Optional[Question] = None
first_question_key: str = ""
questions: Dict[str, Question] = {}
questions_definition = InteractiveFlowCreator._parse_questions_definition(flow_definition_path, extra_context)
try:
for question in questions_definition.get("questions", []):
q = QuestionFactory.create_question_from_json(question)
if not first_question_key:
first_question_key = q.key
elif previous_question and not previous_question.default_next_question_key:
previous_question.set_default_next_question_key(q.key)
questions[q.key] = q
previous_question = q
return questions, first_question_key
except (KeyError, ValueError, AttributeError, TypeError) as ex:
raise QuestionsFailedParsingException(f"Failed to parse questions: {str(ex)}") from ex
@staticmethod
def _parse_questions_definition(file_path: str, extra_context: Optional[Dict] = None) -> Dict:
"""
Read the questions definition file, do variable substitution, parse it as JSON/YAML
Parameters
----------
file_path : string
Path to the questions definition to read
extra_context : Dict
if the file contains variable($variableName) this parameter provides the values for those variables.
Raises
------
QuestionsNotFoundException: if the file_path doesn't exist
QuestionsFailedParsingException: if any error occurred during variables substitution or content parsing
Returns
-------
questions data as a dictionary
"""
try:
return parse_yaml_file(file_path=file_path, extra_context=extra_context)
except FileNotFoundError as ex:
raise QuestionsNotFoundException(f"questions definition file not found at {file_path}") from ex
except (KeyError, ValueError, yaml.YAMLError) as ex:
raise QuestionsFailedParsingException(f"Failed to parse questions: {str(ex)}") from ex