Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/commands.py
2624 views
1
import logging
2
import os
3
4
from botocore import model
5
from botocore.compat import OrderedDict
6
from botocore.validate import validate_parameters
7
8
import awscli
9
from awscli.argparser import ArgTableArgParser
10
from awscli.argprocess import unpack_argument, unpack_cli_arg
11
from awscli.arguments import CustomArgument, create_argument_model_from_schema
12
from awscli.clidocs import OperationDocumentEventHandler
13
from awscli.clidriver import CLICommand
14
from awscli.bcdoc import docevents
15
from awscli.help import HelpCommand
16
from awscli.schema import SchemaTransformer
17
18
LOG = logging.getLogger(__name__)
19
_open = open
20
21
22
class _FromFile(object):
23
24
def __init__(self, *paths, **kwargs):
25
"""
26
``**kwargs`` can contain a ``root_module`` argument
27
that contains the root module where the file contents
28
should be searched. This is an optional argument, and if
29
no value is provided, will default to ``awscli``. This means
30
that by default we look for examples in the ``awscli`` module.
31
32
"""
33
self.filename = None
34
if paths:
35
self.filename = os.path.join(*paths)
36
if 'root_module' in kwargs:
37
self.root_module = kwargs['root_module']
38
else:
39
self.root_module = awscli
40
41
42
class BasicCommand(CLICommand):
43
44
"""Basic top level command with no subcommands.
45
46
If you want to create a new command, subclass this and
47
provide the values documented below.
48
49
"""
50
51
# This is the name of your command, so if you want to
52
# create an 'aws mycommand ...' command, the NAME would be
53
# 'mycommand'
54
NAME = 'commandname'
55
# This is the description that will be used for the 'help'
56
# command.
57
DESCRIPTION = 'describe the command'
58
# This is optional, if you are fine with the default synopsis
59
# (the way all the built in operations are documented) then you
60
# can leave this empty.
61
SYNOPSIS = ''
62
# If you want to provide some hand written examples, you can do
63
# so here. This is written in RST format. This is optional,
64
# you don't have to provide any examples, though highly encouraged!
65
EXAMPLES = ''
66
# If your command has arguments, you can specify them here. This is
67
# somewhat of an implementation detail, but this is a list of dicts
68
# where the dicts match the kwargs of the CustomArgument's __init__.
69
# For example, if I want to add a '--argument-one' and an
70
# '--argument-two' command, I'd say:
71
#
72
# ARG_TABLE = [
73
# {'name': 'argument-one', 'help_text': 'This argument does foo bar.',
74
# 'action': 'store', 'required': False, 'cli_type_name': 'string',},
75
# {'name': 'argument-two', 'help_text': 'This argument does some other thing.',
76
# 'action': 'store', 'choices': ['a', 'b', 'c']},
77
# ]
78
#
79
# A `schema` parameter option is available to accept a custom JSON
80
# structure as input. See the file `awscli/schema.py` for more info.
81
ARG_TABLE = []
82
# If you want the command to have subcommands, you can provide a list of
83
# dicts. We use a list here because we want to allow a user to provide
84
# the order they want to use for subcommands.
85
# SUBCOMMANDS = [
86
# {'name': 'subcommand1', 'command_class': SubcommandClass},
87
# {'name': 'subcommand2', 'command_class': SubcommandClass2},
88
# ]
89
# The command_class must subclass from ``BasicCommand``.
90
SUBCOMMANDS = []
91
92
FROM_FILE = _FromFile
93
# You can set the DESCRIPTION, SYNOPSIS, and EXAMPLES to FROM_FILE
94
# and we'll automatically read in that data from the file.
95
# This is useful if you have a lot of content and would prefer to keep
96
# the docs out of the class definition. For example:
97
#
98
# DESCRIPTION = FROM_FILE
99
#
100
# will set the DESCRIPTION value to the contents of
101
# awscli/examples/<command name>/_description.rst
102
# The naming conventions for these attributes are:
103
#
104
# DESCRIPTION = awscli/examples/<command name>/_description.rst
105
# SYNOPSIS = awscli/examples/<command name>/_synopsis.rst
106
# EXAMPLES = awscli/examples/<command name>/_examples.rst
107
#
108
# You can also provide a relative path and we'll load the file
109
# from the specified location:
110
#
111
# DESCRIPTION = awscli/examples/<filename>
112
#
113
# For example:
114
#
115
# DESCRIPTION = FROM_FILE('command, 'subcommand, '_description.rst')
116
# DESCRIPTION = 'awscli/examples/command/subcommand/_description.rst'
117
#
118
119
# At this point, the only other thing you have to implement is a _run_main
120
# method (see the method for more information).
121
122
def __init__(self, session):
123
self._session = session
124
self._arg_table = None
125
self._subcommand_table = None
126
self._lineage = [self]
127
128
def __call__(self, args, parsed_globals):
129
# args is the remaining unparsed args.
130
# We might be able to parse these args so we need to create
131
# an arg parser and parse them.
132
self._subcommand_table = self._build_subcommand_table()
133
self._arg_table = self._build_arg_table()
134
event = 'before-building-argument-table-parser.%s' % \
135
".".join(self.lineage_names)
136
self._session.emit(event, argument_table=self._arg_table, args=args,
137
session=self._session, parsed_globals=parsed_globals)
138
parser = ArgTableArgParser(self.arg_table, self.subcommand_table)
139
parsed_args, remaining = parser.parse_known_args(args)
140
141
# Unpack arguments
142
for key, value in vars(parsed_args).items():
143
cli_argument = None
144
145
# Convert the name to use dashes instead of underscore
146
# as these are how the parameters are stored in the
147
# `arg_table`.
148
xformed = key.replace('_', '-')
149
if xformed in self.arg_table:
150
cli_argument = self.arg_table[xformed]
151
152
value = unpack_argument(
153
self._session,
154
'custom',
155
self.name,
156
cli_argument,
157
value,
158
parsed_globals
159
)
160
161
# If this parameter has a schema defined, then allow plugins
162
# a chance to process and override its value.
163
if self._should_allow_plugins_override(cli_argument, value):
164
override = self._session\
165
.emit_first_non_none_response(
166
'process-cli-arg.%s.%s' % ('custom', self.name),
167
cli_argument=cli_argument, value=value, operation=None)
168
169
if override is not None:
170
# A plugin supplied a conversion
171
value = override
172
else:
173
# Unpack the argument, which is a string, into the
174
# correct Python type (dict, list, etc)
175
value = unpack_cli_arg(cli_argument, value)
176
self._validate_value_against_schema(
177
cli_argument.argument_model, value)
178
179
setattr(parsed_args, key, value)
180
181
if hasattr(parsed_args, 'help'):
182
self._display_help(parsed_args, parsed_globals)
183
elif getattr(parsed_args, 'subcommand', None) is None:
184
# No subcommand was specified so call the main
185
# function for this top level command.
186
if remaining:
187
raise ValueError("Unknown options: %s" % ','.join(remaining))
188
return self._run_main(parsed_args, parsed_globals)
189
else:
190
return self.subcommand_table[parsed_args.subcommand](remaining,
191
parsed_globals)
192
193
def _validate_value_against_schema(self, model, value):
194
validate_parameters(value, model)
195
196
def _should_allow_plugins_override(self, param, value):
197
if (param and param.argument_model is not None and
198
value is not None):
199
return True
200
return False
201
202
def _run_main(self, parsed_args, parsed_globals):
203
# Subclasses should implement this method.
204
# parsed_globals are the parsed global args (things like region,
205
# profile, output, etc.)
206
# parsed_args are any arguments you've defined in your ARG_TABLE
207
# that are parsed. These will come through as whatever you've
208
# provided as the 'dest' key. Otherwise they default to the
209
# 'name' key. For example: ARG_TABLE[0] = {"name": "foo-arg", ...}
210
# can be accessed by ``parsed_args.foo_arg``.
211
raise NotImplementedError("_run_main")
212
213
def _build_subcommand_table(self):
214
subcommand_table = OrderedDict()
215
for subcommand in self.SUBCOMMANDS:
216
subcommand_name = subcommand['name']
217
subcommand_class = subcommand['command_class']
218
subcommand_table[subcommand_name] = subcommand_class(self._session)
219
self._session.emit('building-command-table.%s' % self.NAME,
220
command_table=subcommand_table,
221
session=self._session,
222
command_object=self)
223
self._add_lineage(subcommand_table)
224
return subcommand_table
225
226
def _display_help(self, parsed_args, parsed_globals):
227
help_command = self.create_help_command()
228
help_command(parsed_args, parsed_globals)
229
230
def create_help_command(self):
231
command_help_table = {}
232
if self.SUBCOMMANDS:
233
command_help_table = self.create_help_command_table()
234
return BasicHelp(self._session, self, command_table=command_help_table,
235
arg_table=self.arg_table)
236
237
def create_help_command_table(self):
238
"""
239
Create the command table into a form that can be handled by the
240
BasicDocHandler.
241
"""
242
commands = {}
243
for command in self.SUBCOMMANDS:
244
commands[command['name']] = command['command_class'](self._session)
245
self._add_lineage(commands)
246
return commands
247
248
def _build_arg_table(self):
249
arg_table = OrderedDict()
250
self._session.emit('building-arg-table.%s' % self.NAME,
251
arg_table=self.ARG_TABLE)
252
for arg_data in self.ARG_TABLE:
253
254
# If a custom schema was passed in, create the argument_model
255
# so that it can be validated and docs can be generated.
256
if 'schema' in arg_data:
257
argument_model = create_argument_model_from_schema(
258
arg_data.pop('schema'))
259
arg_data['argument_model'] = argument_model
260
custom_argument = CustomArgument(**arg_data)
261
262
arg_table[arg_data['name']] = custom_argument
263
return arg_table
264
265
def _add_lineage(self, command_table):
266
for command in command_table:
267
command_obj = command_table[command]
268
command_obj.lineage = self.lineage + [command_obj]
269
270
@property
271
def arg_table(self):
272
if self._arg_table is None:
273
self._arg_table = self._build_arg_table()
274
return self._arg_table
275
276
@property
277
def subcommand_table(self):
278
if self._subcommand_table is None:
279
self._subcommand_table = self._build_subcommand_table()
280
return self._subcommand_table
281
282
@classmethod
283
def add_command(cls, command_table, session, **kwargs):
284
command_table[cls.NAME] = cls(session)
285
286
@property
287
def name(self):
288
return self.NAME
289
290
@property
291
def lineage(self):
292
return self._lineage
293
294
@lineage.setter
295
def lineage(self, value):
296
self._lineage = value
297
298
299
class BasicHelp(HelpCommand):
300
301
def __init__(self, session, command_object, command_table, arg_table,
302
event_handler_class=None):
303
super(BasicHelp, self).__init__(session, command_object,
304
command_table, arg_table)
305
# This is defined in HelpCommand so we're matching the
306
# casing here.
307
if event_handler_class is None:
308
event_handler_class = BasicDocHandler
309
self.EventHandlerClass = event_handler_class
310
311
# These are public attributes that are mapped from the command
312
# object. These are used by the BasicDocHandler below.
313
self._description = command_object.DESCRIPTION
314
self._synopsis = command_object.SYNOPSIS
315
self._examples = command_object.EXAMPLES
316
317
@property
318
def name(self):
319
return self.obj.NAME
320
321
@property
322
def description(self):
323
return self._get_doc_contents('_description')
324
325
@property
326
def synopsis(self):
327
return self._get_doc_contents('_synopsis')
328
329
@property
330
def examples(self):
331
return self._get_doc_contents('_examples')
332
333
@property
334
def event_class(self):
335
return '.'.join(self.obj.lineage_names)
336
337
def _get_doc_contents(self, attr_name):
338
value = getattr(self, attr_name)
339
if isinstance(value, BasicCommand.FROM_FILE):
340
if value.filename is not None:
341
trailing_path = value.filename
342
else:
343
trailing_path = os.path.join(self.name, attr_name + '.rst')
344
root_module = value.root_module
345
doc_path = os.path.join(
346
os.path.abspath(os.path.dirname(root_module.__file__)),
347
'examples', trailing_path)
348
with _open(doc_path) as f:
349
return f.read()
350
else:
351
return value
352
353
def __call__(self, args, parsed_globals):
354
# Create an event handler for a Provider Document
355
instance = self.EventHandlerClass(self)
356
# Now generate all of the events for a Provider document.
357
# We pass ourselves along so that we can, in turn, get passed
358
# to all event handlers.
359
docevents.generate_events(self.session, self)
360
self.renderer.render(self.doc.getvalue())
361
instance.unregister()
362
363
364
class BasicDocHandler(OperationDocumentEventHandler):
365
366
def __init__(self, help_command):
367
super(BasicDocHandler, self).__init__(help_command)
368
self.doc = help_command.doc
369
370
def doc_description(self, help_command, **kwargs):
371
self.doc.style.h2('Description')
372
self.doc.write(help_command.description)
373
self.doc.style.new_paragraph()
374
375
def doc_synopsis_start(self, help_command, **kwargs):
376
if not help_command.synopsis:
377
super(BasicDocHandler, self).doc_synopsis_start(
378
help_command=help_command, **kwargs)
379
else:
380
self.doc.style.h2('Synopsis')
381
self.doc.style.start_codeblock()
382
self.doc.writeln(help_command.synopsis)
383
384
def doc_synopsis_option(self, arg_name, help_command, **kwargs):
385
if not help_command.synopsis:
386
doc = help_command.doc
387
argument = help_command.arg_table[arg_name]
388
if argument.synopsis:
389
option_str = argument.synopsis
390
elif argument.group_name in self._arg_groups:
391
if argument.group_name in self._documented_arg_groups:
392
# This arg is already documented so we can move on.
393
return
394
option_str = ' | '.join(
395
[a.cli_name for a in
396
self._arg_groups[argument.group_name]])
397
self._documented_arg_groups.append(argument.group_name)
398
elif argument.cli_type_name == 'boolean':
399
option_str = '%s' % argument.cli_name
400
elif argument.nargs == '+':
401
option_str = "%s <value> [<value>...]" % argument.cli_name
402
else:
403
option_str = '%s <value>' % argument.cli_name
404
if not (argument.required or argument.positional_arg):
405
option_str = '[%s]' % option_str
406
doc.writeln('%s' % option_str)
407
408
else:
409
# A synopsis has been provided so we don't need to write
410
# anything here.
411
pass
412
413
def doc_synopsis_end(self, help_command, **kwargs):
414
if not help_command.synopsis and not help_command.command_table:
415
super(BasicDocHandler, self).doc_synopsis_end(
416
help_command=help_command, **kwargs)
417
else:
418
self.doc.style.end_codeblock()
419
420
def doc_global_option(self, help_command, **kwargs):
421
if not help_command.command_table:
422
super().doc_global_option(help_command, **kwargs)
423
424
def doc_examples(self, help_command, **kwargs):
425
if help_command.examples:
426
self.doc.style.h2('Examples')
427
self.doc.write(help_command.examples)
428
429
def doc_subitems_start(self, help_command, **kwargs):
430
if help_command.command_table:
431
doc = help_command.doc
432
doc.style.h2('Available Commands')
433
doc.style.toctree()
434
435
def doc_subitem(self, command_name, help_command, **kwargs):
436
if help_command.command_table:
437
doc = help_command.doc
438
doc.style.tocitem(command_name)
439
440
def doc_subitems_end(self, help_command, **kwargs):
441
pass
442
443
def doc_output(self, help_command, event_name, **kwargs):
444
pass
445
446