Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aws
GitHub Repository: aws/aws-cli
Path: blob/develop/awscli/customizations/flatten.py
1566 views
1
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
#
3
# Licensed under the Apache License, Version 2.0 (the "License"). You
4
# may not use this file except in compliance with the License. A copy of
5
# the License is located at
6
#
7
# http://aws.amazon.com/apache2.0/
8
#
9
# or in the "license" file accompanying this file. This file is
10
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11
# ANY KIND, either express or implied. See the License for the specific
12
# language governing permissions and limitations under the License.
13
14
import logging
15
16
from awscli.arguments import CustomArgument
17
18
LOG = logging.getLogger(__name__)
19
20
# Nested argument member separator
21
SEP = '.'
22
23
24
class FlattenedArgument(CustomArgument):
25
"""
26
A custom argument which has been flattened from an existing structure. When
27
added to the call params it is hydrated back into the structure.
28
29
Supports both an object and a list of objects, in which case the flattened
30
parameters will hydrate a list with a single object in it.
31
"""
32
def __init__(self, name, container, prop, help_text='', required=None,
33
type=None, hydrate=None, hydrate_value=None):
34
self.type = type
35
self._container = container
36
self._property = prop
37
self._hydrate = hydrate
38
self._hydrate_value = hydrate_value
39
super(FlattenedArgument, self).__init__(name=name, help_text=help_text,
40
required=required)
41
42
@property
43
def cli_type_name(self):
44
return self.type
45
46
def add_to_params(self, parameters, value):
47
"""
48
Hydrate the original structure with the value of this flattened
49
argument.
50
51
TODO: This does not hydrate nested structures (``XmlName1.XmlName2``)!
52
To do this for now you must provide your own ``hydrate`` method.
53
"""
54
container = self._container.argument_model.name
55
cli_type = self._container.cli_type_name
56
key = self._property
57
58
LOG.debug('Hydrating {0}[{1}]'.format(container, key))
59
60
if value is not None:
61
# Convert type if possible
62
if self.type == 'boolean':
63
value = not value.lower() == 'false'
64
elif self.type in ['integer', 'long']:
65
value = int(value)
66
elif self.type in ['float', 'double']:
67
value = float(value)
68
69
if self._hydrate:
70
self._hydrate(parameters, container, cli_type, key, value)
71
else:
72
if container not in parameters:
73
if cli_type == 'list':
74
parameters[container] = [{}]
75
else:
76
parameters[container] = {}
77
78
if self._hydrate_value:
79
value = self._hydrate_value(value)
80
81
if cli_type == 'list':
82
parameters[container][0][key] = value
83
else:
84
parameters[container][key] = value
85
86
87
class FlattenArguments(object):
88
"""
89
Flatten arguments for one or more commands for a particular service from
90
a given configuration which maps service call parameters to flattened
91
names. Takes in a configuration dict of the form::
92
93
{
94
"command-cli-name": {
95
"argument-cli-name": {
96
"keep": False,
97
"flatten": {
98
"XmlName": {
99
"name": "flattened-cli-name",
100
"type": "Optional custom type",
101
"required": "Optional custom required",
102
"help_text": "Optional custom docs",
103
"hydrate_value": Optional function to hydrate value,
104
"hydrate": Optional function to hydrate
105
},
106
...
107
}
108
},
109
...
110
},
111
...
112
}
113
114
The ``type``, ``required`` and ``help_text`` arguments are entirely
115
optional and by default are pulled from the model. You should only set them
116
if you wish to override the default values in the model.
117
118
The ``keep`` argument determines whether the original command is still
119
accessible vs. whether it is removed. It defaults to ``False`` if not
120
present, which removes the original argument.
121
122
The keys inside of ``flatten`` (e.g. ``XmlName`` above) can include nested
123
references to structures via a colon. For example, ``XmlName1:XmlName2``
124
for the following structure::
125
126
{
127
"XmlName1": {
128
"XmlName2": ...
129
}
130
}
131
132
The ``hydrate_value`` function takes in a value and should return a value.
133
It is only called when the value is not ``None``. Example::
134
135
"hydrate_value": lambda (value): value.upper()
136
137
The ``hydrate`` function takes in a list of existing parameters, the name
138
of the container, its type, the name of the container key and its set
139
value. For the example above, the container would be
140
``'argument-cli-name'``, the key would be ``'XmlName'`` and the value
141
whatever the user passed in. Example::
142
143
def my_hydrate(params, container, cli_type, key, value):
144
if container not in params:
145
params[container] = {'default': 'values'}
146
147
params[container][key] = value
148
149
It's possible for ``cli_type`` to be ``list``, in which case you should
150
ensure that a list of one or more objects is hydrated rather than a
151
single object.
152
"""
153
def __init__(self, service_name, configs):
154
self.configs = configs
155
self.service_name = service_name
156
157
def register(self, cli):
158
"""
159
Register with a CLI instance, listening for events that build the
160
argument table for operations in the configuration dict.
161
"""
162
# Flatten each configured operation when they are built
163
service = self.service_name
164
for operation in self.configs:
165
cli.register('building-argument-table.{0}.{1}'.format(service,
166
operation),
167
self.flatten_args)
168
169
def flatten_args(self, command, argument_table, **kwargs):
170
# For each argument with a bag of parameters
171
for name, argument in self.configs[command.name].items():
172
argument_from_table = argument_table[name]
173
overwritten = False
174
175
LOG.debug('Flattening {0} argument {1} into {2}'.format(
176
command.name, name,
177
', '.join([v['name'] for k, v in argument['flatten'].items()])
178
))
179
180
# For each parameter to flatten out
181
for sub_argument, new_config in argument['flatten'].items():
182
config = new_config.copy()
183
config['container'] = argument_from_table
184
config['prop'] = sub_argument
185
186
# Handle nested arguments
187
_arg = self._find_nested_arg(
188
argument_from_table.argument_model, sub_argument
189
)
190
191
# Pull out docs and required attribute
192
self._merge_member_config(_arg, sub_argument, config)
193
194
# Create and set the new flattened argument
195
new_arg = FlattenedArgument(**config)
196
argument_table[new_config['name']] = new_arg
197
198
if name == new_config['name']:
199
overwritten = True
200
201
# Delete the original argument?
202
if not overwritten and ('keep' not in argument or
203
not argument['keep']):
204
del argument_table[name]
205
206
def _find_nested_arg(self, argument, name):
207
"""
208
Find and return a nested argument, if it exists. If no nested argument
209
is requested then the original argument is returned. If the nested
210
argument cannot be found, then a ValueError is raised.
211
"""
212
if SEP in name:
213
# Find the actual nested argument to pull out
214
LOG.debug('Finding nested argument in {0}'.format(name))
215
for piece in name.split(SEP)[:-1]:
216
for member_name, member in argument.members.items():
217
if member_name == piece:
218
argument = member
219
break
220
else:
221
raise ValueError('Invalid piece {0}'.format(piece))
222
223
return argument
224
225
def _merge_member_config(self, argument, name, config):
226
"""
227
Merges an existing config taken from the configuration dict with an
228
existing member of an existing argument object. This pulls in
229
attributes like ``required`` and ``help_text`` if they have not been
230
overridden in the configuration dict. Modifies the config in-place.
231
"""
232
# Pull out docs and required attribute
233
for member_name, member in argument.members.items():
234
if member_name == name.split(SEP)[-1]:
235
if 'help_text' not in config:
236
config['help_text'] = member.documentation
237
238
if 'required' not in config:
239
config['required'] = member_name in argument.required_members
240
241
if 'type' not in config:
242
config['type'] = member.type_name
243
244
break
245
246