Skip to content

Commit d2eb67d

Browse files
committed
Proof of concept of server side completion
1 parent 2f16a23 commit d2eb67d

File tree

3 files changed

+159
-11
lines changed

3 files changed

+159
-11
lines changed

awsshell/makeindex.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ def index_command(index_dict, help_command):
3939
'type_name': arg_obj.cli_type_name,
4040
'minidoc': '',
4141
'example': '',
42+
# The name used in the API call/botocore,
43+
# typically CamelCased.
44+
'api_name': getattr(arg_obj, '_serialized_name', '')
4245
}
4346
if arg_obj.documentation:
4447
metadata['minidoc'] = remove_html(arg_obj.documentation.split('\n')[0])
@@ -132,5 +135,9 @@ def main():
132135
args = parser.parse_args()
133136
if args.output is None:
134137
args.output = determine_index_filename()
135-
#write_index(args.output)
136-
write_doc_index()
138+
write_index(args.output)
139+
#write_doc_index()
140+
141+
142+
if __name__ == '__main__':
143+
main()

awsshell/resource/index.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,27 @@
1-
"""Index and retrive information from the resource JSON."""
2-
import jmespath
1+
"""Index and retrive information from the resource JSON.
2+
3+
The classes are organized as follows:
4+
5+
* ResourceIndexBuilder - Takes a boto3 resource and converts into the
6+
index format we need to do server side completions.
7+
* CompleterQuery - Takes the index from ResourceIndexBuilder and looks
8+
up how to perform the autocompletion. Note that this class does
9+
*not* actually do the autocompletion. It merely tells you how
10+
you _would_ do the autocompletion if you made the appropriate
11+
service calls.
12+
* ServerSideCompleter - The thing that does the actual autocompletion.
13+
You tell it the command/operation/param you're on, and it will
14+
return a list of completions for you.
15+
16+
"""
17+
import logging
318
from collections import namedtuple
419

20+
import jmespath
21+
from botocore import xform_name
22+
23+
LOG = logging.getLogger(__name__)
24+
525
# service - The name of the AWS service
626
# operation - The name of the AWS operation
727
# params - A dict of params to send in the request (not implemented yet)
@@ -90,7 +110,9 @@ def describe_autocomplete(self, service, operation, param):
90110
91111
"""
92112
service_index = self._index[service]
113+
LOG.debug(service_index)
93114
if param not in service_index.get('operations', {}).get(operation, {}):
115+
LOG.debug("param not in index: %s", param)
94116
return None
95117
p = service_index['operations'][operation][param]
96118
resource_name = p['resourceName']
@@ -103,6 +125,69 @@ def describe_autocomplete(self, service, operation, param):
103125
params={}, path=path)
104126

105127

128+
class ServerSideCompleter(object):
129+
def __init__(self, session, builder):
130+
# session is a boto3 session.
131+
# It is a public attribute as it is intended to be
132+
# changed if the profile changes.
133+
self.session = session
134+
self._loader = session._loader
135+
self._builder = builder
136+
self._client_cache = {}
137+
self._completer_cache = {}
138+
139+
def _get_completer_for_service(self, service_name, resource_model):
140+
if service_name not in self._completer_cache:
141+
index = self._builder.build_index(resource_model)
142+
cq = CompleterQuery({service_name: index})
143+
self._completer_cache[service_name] = cq
144+
return self._completer_cache[service_name]
145+
146+
def _get_client(self, service_name):
147+
if service_name in self._client_cache:
148+
return self._client_cache[service_name]
149+
client = self.session.client(service_name)
150+
self._client_cache[service_name] = client
151+
return client
152+
153+
def autocomplete(self, service, operation, param):
154+
# Example call:
155+
# service='ec2', operation='terminate-instances',
156+
# param='--instance-ids'.
157+
# We need to convert this to botocore syntax.
158+
# First try to load the resource model.
159+
LOG.debug("Called with: %s, %s, %s", service, operation, param)
160+
try:
161+
resource_model = self._loader.load_service_model(
162+
service, 'resources-1')
163+
except Exception as e:
164+
# No resource == no server side completion.
165+
return
166+
# Now convert operation to the name used by botocore.
167+
client = self._get_client(service)
168+
api_operation_name = client.meta.method_to_api_mapping.get(
169+
operation.replace('-', '_'))
170+
if api_operation_name is None:
171+
return
172+
# Now we need to convert the param name to the
173+
# casing used by the API.
174+
completer = self._get_completer_for_service(service,
175+
resource_model)
176+
result = completer.describe_autocomplete(
177+
service, api_operation_name, param)
178+
if result is None:
179+
return
180+
# DEBUG:awsshell.resource.index:RESULTS:
181+
# ServerCompletion(service=u'ec2', operation=u'DescribeInstances',
182+
# params={}, path=u'Reservations[].Instances[].InstanceId')
183+
try:
184+
response = getattr(client, xform_name(result.operation, '_'))()
185+
except Exception as e:
186+
return
187+
results = jmespath.search(result.path, response)
188+
return results
189+
190+
106191
def main():
107192
import sys
108193
import json

awsshell/shellcomplete.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,14 @@
99
logic, see awsshell.autocomplete.
1010
1111
"""
12+
import logging
1213
from prompt_toolkit.completion import Completer, Completion
1314

1415

16+
logging.basicConfig(filename='/tmp/completions', level=logging.DEBUG)
17+
LOG = logging.getLogger(__name__)
18+
19+
1520
class AWSShellCompleter(Completer):
1621
"""Completer class for the aws-shell.
1722
@@ -20,8 +25,19 @@ class AWSShellCompleter(Completer):
2025
low level, and can be reused in contexts other than the
2126
aws shell.
2227
"""
23-
def __init__(self, completer):
28+
def __init__(self, completer, server_side_completer=None):
2429
self._completer = completer
30+
if server_side_completer is None:
31+
server_side_completer = self._create_server_side_completer()
32+
self._server_side_completer = server_side_completer
33+
34+
def _create_server_side_completer(self):
35+
import boto3.session
36+
from awsshell.resource import index
37+
session = boto3.session.Session()
38+
builder = index.ResourceIndexBuilder()
39+
completer = index.ServerSideCompleter(session, builder)
40+
return completer
2541

2642
@property
2743
def completer(self):
@@ -39,14 +55,17 @@ def last_option(self):
3955
def current_command(self):
4056
return u' '.join(self._completer.cmd_path)
4157

42-
def get_completions(self, document, complete_event):
43-
text_before_cursor = document.text_before_cursor
58+
def _convert_to_prompt_completions(self, low_level_completions,
59+
text_before_cursor):
60+
# Converts the low level completions from the model autocompleter
61+
# and converts them to Completion() objects used by
62+
# prompt_toolkit. We also try to enhance the metadata of the
63+
# completion by including docs and marking required fields.
64+
arg_meta = self._completer.arg_metadata
4465
word_before_cursor = ''
4566
if text_before_cursor.strip():
4667
word_before_cursor = text_before_cursor.strip().split()[-1]
47-
completions = self._completer.autocomplete(text_before_cursor)
48-
arg_meta = self._completer.arg_metadata
49-
for completion in completions:
68+
for completion in low_level_completions:
5069
# Go through the completions and add inline docs and
5170
# mark which options are required.
5271
if completion.startswith('--') and completion in arg_meta:
@@ -69,4 +88,41 @@ def get_completions(self, document, complete_event):
6988
display=display_text, display_meta=display_meta)
7089

7190

72-
91+
def get_completions(self, document, complete_event):
92+
text_before_cursor = document.text_before_cursor
93+
completions = self._completer.autocomplete(text_before_cursor)
94+
prompt_completions = list(self._convert_to_prompt_completions(
95+
completions, text_before_cursor))
96+
LOG.debug("num_completions: %s, text: %s", len(prompt_completions),
97+
text_before_cursor)
98+
if (not prompt_completions and self._completer.last_option and
99+
len(self._completer.cmd_path) == 3):
100+
# If we couldn't complete anything from the JSON model
101+
# completer and we're on a cli option (e.g --foo), we
102+
# can ask the server side completer if it knows anything
103+
# about this resource.
104+
LOG.debug("No local autocompletions found, trying "
105+
"server side completion.")
106+
command = self._completer.cmd_path
107+
service = command[1]
108+
if service == 's3api':
109+
# TODO: we need a more generic way to capture renames
110+
# of commands. This currently lives in the CLI
111+
# customization code.
112+
service = 's3'
113+
operation = command[2]
114+
param = self._completer.arg_metadata.get(
115+
self._completer.last_option, {}).get('api_name')
116+
if param is not None:
117+
results = self._server_side_completer.autocomplete(
118+
service, operation, param)
119+
if results is not None:
120+
for result in results:
121+
# Insert at the end
122+
location = 0
123+
yield Completion(result, location,
124+
display=result,
125+
display_meta='')
126+
else:
127+
for c in prompt_completions:
128+
yield c

0 commit comments

Comments
 (0)