Skip to content

Commit 99a94f9

Browse files
committed
Implemented json conversion, environments and basic wizard flow.
1 parent 8950f03 commit 99a94f9

File tree

3 files changed

+228
-0
lines changed

3 files changed

+228
-0
lines changed

awsshell/app.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,19 @@ def run(self, command, application):
151151
return EXIT_REQUESTED
152152

153153

154+
class WizardHandler(object):
155+
def run(self, command, application):
156+
raise Exception("TODO")
157+
158+
154159
class DotCommandHandler(object):
155160
HANDLER_CLASSES = {
156161
'edit': EditHandler,
157162
'profile': ProfileHandler,
158163
'cd': ChangeDirHandler,
159164
'exit': ExitHandler,
160165
'quit': ExitHandler,
166+
'wizard': WizardHandler,
161167
}
162168

163169
def __init__(self, output=sys.stdout, err=sys.stderr):

awsshell/wizard.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import re
2+
import json
3+
import jmespath
4+
import botocore.session
5+
6+
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()
11+
12+
13+
class Wizard(object):
14+
15+
def __init__(self, filename=None):
16+
self._env = Environment()
17+
if filename:
18+
self.load_from_json(filename)
19+
20+
# Loads the wizards from the spec in dict form
21+
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?
24+
self.stages = {}
25+
for s in spec['Stages']:
26+
stage = Stage(s, self._env)
27+
self.stages[stage.name] = stage
28+
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+
def execute(self):
38+
current_stage = self.start_stage
39+
while current_stage:
40+
stage = self.stages.get(current_stage, None)
41+
if not stage:
42+
raise Exception("Stage does not exist: %s" % current_stage)
43+
stage.execute()
44+
current_stage = stage.get_next_stage()
45+
46+
47+
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))
56+
57+
def __handle_static_retrieval(self):
58+
return self.retrieval.get('Resource')
59+
60+
def __handle_request_retrieval(self):
61+
# TODO very basic requests... refactor needed
62+
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'])
66+
# get the operation from the client
67+
operation = getattr(client, camel_to_snake(req['Operation']))
68+
# get any parameters
69+
parameters = req.get('Parameters', {})
70+
env_parameters = self._resolve_parameters(req.get('EnvParameters', {}))
71+
# union of parameters and env_parameters, conflicts favor env_params
72+
parameters = dict(parameters, **env_parameters)
73+
# 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
78+
79+
def _handle_retrieval(self):
80+
print(self.prompt)
81+
# In case of no retrieval, empty dict
82+
if not self.retrieval:
83+
return {}
84+
elif self.retrieval['Type'] == 'Static':
85+
return self.__handle_static_retrieval()
86+
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
93+
94+
def _handle_interaction(self, data):
95+
# TODO actually implement this step
96+
# if no interaction step, just forward data
97+
if not self.interaction:
98+
return data
99+
elif self.interaction['ScreenType'] == 'SimpleSelect':
100+
data = data[0]
101+
elif self.interaction['ScreenType'] == 'SimplePrompt':
102+
for field in data:
103+
data[field] = '500'
104+
return data
105+
106+
def _handle_resolution(self, data):
107+
if self.resolution:
108+
if self.resolution.get('Path'):
109+
data = jmespath.search(self.resolution['Path'], data)
110+
self._wizard_env.store(self.resolution['Key'], data)
111+
112+
def get_next_stage(self):
113+
if not self.next_stage:
114+
return None
115+
elif self.next_stage['Type'] == 'Name':
116+
return self.next_stage['Name']
117+
elif self.next_stage['Type'] == 'Variable':
118+
return self._wizard_env.retrieve(self.next_stage['Name'])
119+
120+
# Executes all three steps of the stage
121+
def execute(self):
122+
retrieved = self._handle_retrieval()
123+
transformed = self._handle_interaction(retrieved)
124+
self._handle_resolution(transformed)
125+
126+
127+
class Environment(object):
128+
129+
def __init__(self):
130+
self._variables = {}
131+
132+
def store(self, key, val):
133+
self._variables[key] = val
134+
135+
def retrieve(self, path):
136+
return jmespath.search(path, self._variables)

tests/unit/test_wizard.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import unittest
2+
from awsshell.wizard import Wizard
3+
from awsshell.wizard import Environment
4+
from awsshell.wizard import Stage
5+
6+
7+
class EnvironmentTest(unittest.TestCase):
8+
9+
# Set up a sample environment
10+
def setUp(self):
11+
self.var = {'epic': 'nice'}
12+
self.env = Environment()
13+
self.env.store('env_var', self.var)
14+
15+
# Test that the environment properly stores the given var
16+
def test_environment_store(self):
17+
self.assertEqual(self.env._variables.get('env_var'), self.var)
18+
19+
# Test that the env can retrieve keys via jmespath queries
20+
def test_environment_retrieve(self):
21+
self.assertEqual(self.env.retrieve('env_var'), self.var)
22+
self.assertEqual(self.env.retrieve('env_var.epic'), 'nice')
23+
24+
25+
class StageTest(unittest.TestCase):
26+
27+
# Set up a sample stage
28+
def setUp(self):
29+
self.wiz = Wizard()
30+
self.retrieval = {
31+
'Type': 'Static',
32+
'Resource': [
33+
{'Option': 'Create new Api', 'Stage': 'CreateApi'},
34+
{
35+
'Option': 'Generate new Api from swagger spec file',
36+
'Stage': 'NewSwaggerApi'
37+
}
38+
]
39+
}
40+
self.interaction = {'ScreenType': 'SimpleSelect'},
41+
self.resolution = {'Path': 'Stage', 'Key': 'CreationType'}
42+
self.next_stage = {'Type': 'Variable', 'Name': 'CreationType'}
43+
self.stage_spec = {
44+
'Name': 'ApiSourceSwitch',
45+
'Prompt': 'Prompting',
46+
'Retrieval': self.retrieval,
47+
'Interaction': self.interaction,
48+
'Resolution': self.resolution,
49+
'NextStage': self.next_stage
50+
}
51+
52+
# Test that the spec is translated to the correct attrs
53+
def test_from_spec(self):
54+
test_env = Environment()
55+
stage = Stage(self.stage_spec, test_env)
56+
self.assertEqual(stage.name, 'ApiSourceSwitch')
57+
self.assertEqual(stage.prompt, 'Prompting')
58+
self.assertEqual(stage.retrieval, self.retrieval)
59+
self.assertEqual(stage.interaction, self.interaction)
60+
self.assertEqual(stage.resolution, self.resolution)
61+
self.assertEqual(stage.next_stage, self.next_stage)
62+
63+
# Test that static retrieval reads the data straight from the spec
64+
def test_static_retrieval(self):
65+
test_env = Environment()
66+
stage = Stage(self.stage_spec, test_env)
67+
ret = stage._handle_retrieval()
68+
self.assertEqual(ret, self.retrieval['Resource'])
69+
70+
# Test that resolution properly puts the resolved value into the env
71+
def test_handle_resolution(self):
72+
test_env = Environment()
73+
stage = Stage(self.stage_spec, test_env)
74+
data = {'Stage': 'EpicNice'}
75+
stage._handle_resolution(data)
76+
self.assertEqual(test_env.retrieve('CreationType'), 'EpicNice')
77+
78+
# Test that env paramaters can be resolved for the stage
79+
def test_resolve_parameters(self):
80+
test_env = Environment()
81+
test_env.store('Epic', 'Nice')
82+
test_env.store('Test', {'k': 'v'})
83+
keys = {'a': 'Epic', 'b': 'Test.k'}
84+
stage = Stage(self.stage_spec, test_env)
85+
resolved = stage._resolve_parameters(keys)
86+
self.assertEqual(resolved, {'a': 'Nice', 'b': 'v'})

0 commit comments

Comments
 (0)