1
- import re
1
+ import sys
2
2
import json
3
3
import jmespath
4
4
import botocore .session
5
+ from botocore import xform_name
6
+ from awsshell .resource import index
5
7
6
8
7
- # TODO copy pasta'd from stackoverflow... maybe we have something else?
8
- def camel_to_snake (name ):
9
- s1 = re .sub ('(.)([A-Z][a-z]+)' , r'\1_\2' , name )
10
- return re .sub ('([a-z0-9])([A-Z])' , r'\1_\2' , s1 ).lower ()
9
+ class WizardException (Exception ):
10
+ """Base exception class for the Wizards"""
11
+ pass
11
12
12
13
13
- class Wizard (object ):
14
+ class WizardLoader (object ):
15
+ """This class is responsible for searching various paths to locate wizard
16
+ models. Given a wizard name it will return a wizard object representing the
17
+ wizard. Delegates to botocore for finding and loading the JSON models.
18
+ """
19
+
20
+ def __init__ (self ):
21
+ self ._session = botocore .session .get_session ()
22
+ self ._loader = self ._session .get_component ('data_loader' )
23
+
24
+ def load_wizard (self , name ):
25
+ """Given a wizard's name, returns an instance of that wizard.
14
26
15
- def __init__ (self , filename = None ):
16
- self ._env = Environment ()
17
- if filename :
18
- self .load_from_json (filename )
27
+ :type name: str
28
+ :param name: The name of the desired wizard.
29
+ """
30
+ # TODO possible naming collisions here, always pick first for now
31
+ # Need to discuss and specify wizard invocation
32
+ services = self ._loader .list_available_services (type_name = name )
33
+ model = self ._loader .load_service_model (services [0 ], name )
34
+ return Wizard (model )
35
+
36
+
37
+ class Wizard (object ):
38
+ """The main wizard object containing all of the stages, the environment,
39
+ botocore sessions, and the logic to drive the wizards.
40
+ """
41
+
42
+ def __init__ (self , spec = None ):
43
+ """Constructs a new Wizard
44
+
45
+ :type spec: dict
46
+ :param spec: (Optional) Constructs the wizard by loading the given
47
+ model specification.
48
+ """
49
+ self .env = Environment ()
50
+ self ._session = botocore .session .get_session ()
51
+ self ._cached_creator = index .CachedClientCreator (self ._session )
52
+ if spec :
53
+ self .load_from_dict (spec )
19
54
20
- # Loads the wizards from the spec in dict form
21
55
def load_from_dict (self , spec ):
22
- self .start_stage = spec .get ('StartStage' )
23
- # TODO if no start stage raise exception? default to first in array?
56
+ """Loads the model specification from a dict, populating the
57
+ wizard object.
58
+
59
+ :type spec: dict
60
+ :param spec: The model specification.
61
+
62
+ :raises: :class:`WizardException`
63
+ """
64
+ self .start_stage = spec .get ('StartStage' , None )
65
+ if not self .start_stage :
66
+ raise WizardException ("Start stage not specified" )
24
67
self .stages = {}
25
68
for s in spec ['Stages' ]:
26
- stage = Stage (s , self . _env )
69
+ stage = Stage (s , self )
27
70
self .stages [stage .name ] = stage
28
71
29
- # TODO temporary loader from file, will likely deprecate this when
30
- # there's a systemic way of loading all wizards in specified locations
31
- def load_from_json (self , filename ):
32
- with open (filename , 'r' ) as f :
33
- spec = json .load (f )
34
- self .load_from_dict (spec )
35
-
36
- # Performs the basic loop for progressing stages
37
72
def execute (self ):
73
+ """Runs the wizard. Executes Stages until a final stage is reached.
74
+
75
+ :raises: :class:WizardException
76
+ """
38
77
current_stage = self .start_stage
39
78
while current_stage :
40
79
stage = self .stages .get (current_stage , None )
41
80
if not stage :
42
- raise Exception ("Stage does not exist : %s" % current_stage )
81
+ raise WizardException ("Stage not found : %s" % current_stage )
43
82
stage .execute ()
44
83
current_stage = stage .get_next_stage ()
84
+ # TODO decouple wizard from all I/O
85
+ sys .stdout .write (str (self .env )+ '\n ' )
45
86
46
87
47
88
class Stage (object ):
48
-
49
- KEYS = ['Name' , 'Prompt' , 'Retrieval' ,
50
- 'Interaction' , 'Resolution' , 'NextStage' ]
51
-
52
- def __init__ (self , spec , env ):
53
- self ._wizard_env = env
54
- for key in Stage .KEYS :
55
- setattr (self , camel_to_snake (key ), spec .get (key , None ))
89
+ """The Stage object contains the meta information for a stage and logic
90
+ required to perform all steps present.
91
+ """
92
+
93
+ def __init__ (self , spec , wizard ):
94
+ """Constructs a new Stage object.
95
+
96
+ :type spec: dict
97
+ :param spec: The stage specification model.
98
+
99
+ :type wizard: :class:`Wizard`
100
+ :param wizard: The wizard that this stage is a part of.
101
+ """
102
+ self ._wizard = wizard
103
+ self .name = spec .get ('Name' , None )
104
+ self .prompt = spec .get ('Prompt' , None )
105
+ self .retrieval = spec .get ('Retrieval' , None )
106
+ self .next_stage = spec .get ('NextStage' , None )
107
+ self .resolution = spec .get ('Resolution' , None )
108
+ self .interaction = spec .get ('Interaction' , None )
56
109
57
110
def __handle_static_retrieval (self ):
58
111
return self .retrieval .get ('Resource' )
59
112
60
113
def __handle_request_retrieval (self ):
61
- # TODO very basic requests... refactor needed
62
114
req = self .retrieval ['Resource' ]
63
- # initialize botocore session and client for service in the request
64
- session = botocore .session .get_session ()
65
- client = session .create_client (req ['Service' ])
115
+ # get client from wizard's cache
116
+ client = self ._wizard ._cached_creator .create_client (req ['Service' ])
66
117
# get the operation from the client
67
- operation = getattr (client , camel_to_snake (req ['Operation' ]))
118
+ operation = getattr (client , xform_name (req ['Operation' ]))
68
119
# get any parameters
69
120
parameters = req .get ('Parameters' , {})
70
- env_parameters = self ._resolve_parameters (req .get ('EnvParameters' , {}))
121
+ env_parameters = \
122
+ self ._wizard .env .resolve_parameters (req .get ('EnvParameters' , {}))
71
123
# union of parameters and env_parameters, conflicts favor env_params
72
124
parameters = dict (parameters , ** env_parameters )
73
125
# execute operation passing all parameters
74
- result = operation (** parameters )
75
- if self .retrieval .get ('Path' ):
76
- result = jmespath .search (self .retrieval ['Path' ], result )
77
- return result
126
+ return operation (** parameters )
78
127
79
128
def _handle_retrieval (self ):
80
- print (self .prompt )
129
+ # TODO decouple wizard from all I/O
130
+ sys .stdout .write (self .prompt + '\n ' )
81
131
# In case of no retrieval, empty dict
82
132
if not self .retrieval :
83
133
return {}
84
134
elif self .retrieval ['Type' ] == 'Static' :
85
- return self .__handle_static_retrieval ()
135
+ data = self .__handle_static_retrieval ()
86
136
elif self .retrieval ['Type' ] == 'Request' :
87
- return self .__handle_request_retreival ()
88
-
89
- def _resolve_parameters (self , keys ):
90
- for key in keys :
91
- keys [key ] = self ._wizard_env .retrieve (keys [key ])
92
- return keys
137
+ data = self .__handle_request_retrieval ()
138
+ # Apply JMESPath query if given
139
+ if self .retrieval .get ('Path' ):
140
+ data = jmespath .search (self .retrieval ['Path' ], data )
141
+ return data
93
142
94
143
def _handle_interaction (self , data ):
95
144
# TODO actually implement this step
@@ -100,37 +149,82 @@ def _handle_interaction(self, data):
100
149
data = data [0 ]
101
150
elif self .interaction ['ScreenType' ] == 'SimplePrompt' :
102
151
for field in data :
103
- data [field ] = '500 '
152
+ data [field ] = 'random '
104
153
return data
105
154
106
155
def _handle_resolution (self , data ):
107
156
if self .resolution :
108
157
if self .resolution .get ('Path' ):
109
158
data = jmespath .search (self .resolution ['Path' ], data )
110
- self ._wizard_env .store (self .resolution ['Key' ], data )
159
+ self ._wizard . env .store (self .resolution ['Key' ], data )
111
160
112
161
def get_next_stage (self ):
162
+ """Resolves the next stage name for the stage after this one.
163
+
164
+ :rtype: str
165
+ :return: The name of the next stage.
166
+ """
113
167
if not self .next_stage :
114
168
return None
115
169
elif self .next_stage ['Type' ] == 'Name' :
116
170
return self .next_stage ['Name' ]
117
171
elif self .next_stage ['Type' ] == 'Variable' :
118
- return self ._wizard_env .retrieve (self .next_stage ['Name' ])
172
+ return self ._wizard . env .retrieve (self .next_stage ['Name' ])
119
173
120
174
# Executes all three steps of the stage
121
175
def execute (self ):
122
- retrieved = self ._handle_retrieval ()
123
- transformed = self ._handle_interaction (retrieved )
124
- self ._handle_resolution (transformed )
176
+ """Executes all steps in the stage if they are present.
177
+ 1) Perform Retrieval.
178
+ 2) Perform Interaction on retrieved data.
179
+ 3) Perform Resolution to store data in the environment.
180
+ """
181
+ retrieved_options = self ._handle_retrieval ()
182
+ selected_data = self ._handle_interaction (retrieved_options )
183
+ self ._handle_resolution (selected_data )
125
184
126
185
127
186
class Environment (object ):
187
+ """This class is used to store variables into a dict and retrieve them
188
+ via JMESPath queries instead of normal keys.
189
+ """
128
190
129
191
def __init__ (self ):
130
192
self ._variables = {}
131
193
194
+ def __str__ (self ):
195
+ return json .dumps (self ._variables , indent = 4 , sort_keys = True )
196
+
132
197
def store (self , key , val ):
198
+ """Stores a variable under the given key.
199
+
200
+ :type key: str
201
+ :param key: The key to store the value as.
202
+
203
+ :type val: object
204
+ :param val: The value to store into the environment.
205
+ """
133
206
self ._variables [key ] = val
134
207
135
208
def retrieve (self , path ):
209
+ """Retrieves the variable corresponding to the given JMESPath query.
210
+
211
+ :type path: str
212
+ :param path: The JMESPath query to be used when locating the variable.
213
+ """
136
214
return jmespath .search (path , self ._variables )
215
+
216
+ def resolve_parameters (self , keys ):
217
+ """Resolves all keys in the given keys dict. Expects all values in the
218
+ keys dict to be JMESPath queries to be used when retrieving from the
219
+ environment. Interpolates all values from their path to the actual
220
+ value stored in the environment.
221
+
222
+ :type keys: dict
223
+ :param keys: A dict of keys to paths that need to be resolved.
224
+
225
+ :rtype: dict
226
+ :return: The dict of with all of the paths resolved to their values.
227
+ """
228
+ for key in keys :
229
+ keys [key ] = self .retrieve (keys [key ])
230
+ return keys
0 commit comments