import logging
from awscli.arguments import CustomArgument
LOG = logging.getLogger(__name__)
SEP = '.'
class FlattenedArgument(CustomArgument):
"""
A custom argument which has been flattened from an existing structure. When
added to the call params it is hydrated back into the structure.
Supports both an object and a list of objects, in which case the flattened
parameters will hydrate a list with a single object in it.
"""
def __init__(self, name, container, prop, help_text='', required=None,
type=None, hydrate=None, hydrate_value=None):
self.type = type
self._container = container
self._property = prop
self._hydrate = hydrate
self._hydrate_value = hydrate_value
super(FlattenedArgument, self).__init__(name=name, help_text=help_text,
required=required)
@property
def cli_type_name(self):
return self.type
def add_to_params(self, parameters, value):
"""
Hydrate the original structure with the value of this flattened
argument.
TODO: This does not hydrate nested structures (``XmlName1.XmlName2``)!
To do this for now you must provide your own ``hydrate`` method.
"""
container = self._container.argument_model.name
cli_type = self._container.cli_type_name
key = self._property
LOG.debug('Hydrating {0}[{1}]'.format(container, key))
if value is not None:
if self.type == 'boolean':
value = not value.lower() == 'false'
elif self.type in ['integer', 'long']:
value = int(value)
elif self.type in ['float', 'double']:
value = float(value)
if self._hydrate:
self._hydrate(parameters, container, cli_type, key, value)
else:
if container not in parameters:
if cli_type == 'list':
parameters[container] = [{}]
else:
parameters[container] = {}
if self._hydrate_value:
value = self._hydrate_value(value)
if cli_type == 'list':
parameters[container][0][key] = value
else:
parameters[container][key] = value
class FlattenArguments(object):
"""
Flatten arguments for one or more commands for a particular service from
a given configuration which maps service call parameters to flattened
names. Takes in a configuration dict of the form::
{
"command-cli-name": {
"argument-cli-name": {
"keep": False,
"flatten": {
"XmlName": {
"name": "flattened-cli-name",
"type": "Optional custom type",
"required": "Optional custom required",
"help_text": "Optional custom docs",
"hydrate_value": Optional function to hydrate value,
"hydrate": Optional function to hydrate
},
...
}
},
...
},
...
}
The ``type``, ``required`` and ``help_text`` arguments are entirely
optional and by default are pulled from the model. You should only set them
if you wish to override the default values in the model.
The ``keep`` argument determines whether the original command is still
accessible vs. whether it is removed. It defaults to ``False`` if not
present, which removes the original argument.
The keys inside of ``flatten`` (e.g. ``XmlName`` above) can include nested
references to structures via a colon. For example, ``XmlName1:XmlName2``
for the following structure::
{
"XmlName1": {
"XmlName2": ...
}
}
The ``hydrate_value`` function takes in a value and should return a value.
It is only called when the value is not ``None``. Example::
"hydrate_value": lambda (value): value.upper()
The ``hydrate`` function takes in a list of existing parameters, the name
of the container, its type, the name of the container key and its set
value. For the example above, the container would be
``'argument-cli-name'``, the key would be ``'XmlName'`` and the value
whatever the user passed in. Example::
def my_hydrate(params, container, cli_type, key, value):
if container not in params:
params[container] = {'default': 'values'}
params[container][key] = value
It's possible for ``cli_type`` to be ``list``, in which case you should
ensure that a list of one or more objects is hydrated rather than a
single object.
"""
def __init__(self, service_name, configs):
self.configs = configs
self.service_name = service_name
def register(self, cli):
"""
Register with a CLI instance, listening for events that build the
argument table for operations in the configuration dict.
"""
service = self.service_name
for operation in self.configs:
cli.register('building-argument-table.{0}.{1}'.format(service,
operation),
self.flatten_args)
def flatten_args(self, command, argument_table, **kwargs):
for name, argument in self.configs[command.name].items():
argument_from_table = argument_table[name]
overwritten = False
LOG.debug('Flattening {0} argument {1} into {2}'.format(
command.name, name,
', '.join([v['name'] for k, v in argument['flatten'].items()])
))
for sub_argument, new_config in argument['flatten'].items():
config = new_config.copy()
config['container'] = argument_from_table
config['prop'] = sub_argument
_arg = self._find_nested_arg(
argument_from_table.argument_model, sub_argument
)
self._merge_member_config(_arg, sub_argument, config)
new_arg = FlattenedArgument(**config)
argument_table[new_config['name']] = new_arg
if name == new_config['name']:
overwritten = True
if not overwritten and ('keep' not in argument or
not argument['keep']):
del argument_table[name]
def _find_nested_arg(self, argument, name):
"""
Find and return a nested argument, if it exists. If no nested argument
is requested then the original argument is returned. If the nested
argument cannot be found, then a ValueError is raised.
"""
if SEP in name:
LOG.debug('Finding nested argument in {0}'.format(name))
for piece in name.split(SEP)[:-1]:
for member_name, member in argument.members.items():
if member_name == piece:
argument = member
break
else:
raise ValueError('Invalid piece {0}'.format(piece))
return argument
def _merge_member_config(self, argument, name, config):
"""
Merges an existing config taken from the configuration dict with an
existing member of an existing argument object. This pulls in
attributes like ``required`` and ``help_text`` if they have not been
overridden in the configuration dict. Modifies the config in-place.
"""
for member_name, member in argument.members.items():
if member_name == name.split(SEP)[-1]:
if 'help_text' not in config:
config['help_text'] = member.documentation
if 'required' not in config:
config['required'] = member_name in argument.required_members
if 'type' not in config:
config['type'] = member.type_name
break