Skip to content

Commit

Permalink
Merge pull request #963 from kyleknap/json-skeleton
Browse files Browse the repository at this point in the history
Json skeleton
  • Loading branch information
kyleknap committed Oct 31, 2014
2 parents 27edfcd + eed1362 commit 987c47c
Show file tree
Hide file tree
Showing 20 changed files with 782 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ python:
- "2.7"
- "3.3"
install:
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2; fi
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install unittest2==0.5.1; fi
- pip install -r requirements.txt
- python setup.py develop
- pip install coverage python-coveralls
Expand Down
12 changes: 12 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
CHANGELOG
=========

Next Release (TBD)
==================

* feature:``--generate-cli-skeleton``: Generates a JSON skeleton to fill out
and be used as input to ``--cli-input-json``.
(`issue 963 <https://github.com/aws/aws-cli/pull/963>`_)
* feature:``--cli-input-json``: Runs an operation using a global JSON file
that supplies all of the operation's arguments. This JSON file can
be generated by ``--generate-cli-skeleton``.
(`issue 963 <https://github.com/aws/aws-cli/pull/963>`_)


1.5.4
=====

Expand Down
35 changes: 33 additions & 2 deletions awscli/clidriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,9 @@ def arg_table(self):
def __call__(self, args, parsed_globals):
# Once we know we're trying to call a particular operation
# of a service we can go ahead and load the parameters.
event = 'before-building-argument-table-parser.%s.%s' % \
(self._parent_name, self._name)
self._emit(event, argument_table=self.arg_table, args=args)
operation_parser = self._create_operation_parser(self.arg_table)
self._add_help(operation_parser)
parsed_args, remaining = operation_parser.parse_known_args(args)
Expand All @@ -457,8 +460,32 @@ def __call__(self, args, parsed_globals):
parsed_globals=parsed_globals)
call_parameters = self._build_call_parameters(parsed_args,
self.arg_table)
return self._operation_caller.invoke(
self._operation_object, call_parameters, parsed_globals)
event = 'calling-command.%s.%s' % (self._parent_name,
self._name)
override = self._emit_first_non_none_response(
event,
call_parameters=call_parameters,
parsed_args=parsed_args,
parsed_globals=parsed_globals
)
# There are two possible values for override. It can be some type
# of exception that will be raised if detected or it can represent
# the desired return code. Note that a return code of 0 represents
# a success.
if override is not None:
if isinstance(override, Exception):
# If the override value provided back is an exception then
# raise the exception
raise override
else:
# This is the value usually returned by the ``invoke()``
# method of the operation caller. It represents the return
# code of the operation.
return override
else:
# No override value was supplied.
return self._operation_caller.invoke(
self._operation_object, call_parameters, parsed_globals)

def create_help_command(self):
return OperationHelpCommand(
Expand Down Expand Up @@ -525,6 +552,10 @@ def _emit(self, name, **kwargs):
session = self._service_object.session
return session.emit(name, **kwargs)

def _emit_first_non_none_response(self, name, **kwargs):
session = self._service_object.session
return session.emit_first_non_none_response(name, **kwargs)

def _create_operation_parser(self, arg_table):
parser = ArgTableArgParser(arg_table)
return parser
Expand Down
53 changes: 53 additions & 0 deletions awscli/customizations/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
from awscli.arguments import CustomArgument


class OverrideRequiredArgsArgument(CustomArgument):
"""An argument that if specified makes all other arguments not required
By not required, it refers to not having an error thrown when the
parser does not find an argument that is required on the command line.
To obtain this argument's property of ignoring required arguments,
subclass from this class and fill out the ``ARG_DATA`` parameter as
described below. Note this class is really only useful for subclassing.
"""

# ``ARG_DATA`` follows the same format as a member of ``ARG_TABLE`` in
# ``BasicCommand`` class as specified in
# ``awscli/customizations/commands.py``.
#
# For example, an ``ARG_DATA`` variable would be filled out as:
#
# ARG_DATA =
# {'name': 'my-argument',
# 'help_text': 'This is argument ensures the argument is specified'
# 'no other arguments are required'}
ARG_DATA = {'name': 'no-required-args'}

def __init__(self, session):
self._session = session
self._register_argument_action()
super(OverrideRequiredArgsArgument, self).__init__(**self.ARG_DATA)

def _register_argument_action(self):
self._session.register('before-building-argument-table-parser',
self.override_required_args)

def override_required_args(self, argument_table, args, **kwargs):
name_in_cmdline = '--' + self.name
# Set all ``Argument`` objects in ``argument_table`` to not required
# if this argument's name is present in the command line.
if name_in_cmdline in args:
for arg_name in argument_table.keys():
argument_table[arg_name].required = False
85 changes: 85 additions & 0 deletions awscli/customizations/cliinputjson.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import json

from awscli.paramfile import get_paramfile
from awscli.argprocess import ParamError
from awscli.customizations.arguments import OverrideRequiredArgsArgument


def register_cli_input_json(cli):
cli.register('building-argument-table', add_cli_input_json)


def add_cli_input_json(operation, argument_table, **kwargs):
# This argument cannot support operations with streaming output which
# is designated by the argument name `outfile`.
if 'outfile' not in argument_table:
cli_input_json_argument = CliInputJSONArgument(operation)
cli_input_json_argument.add_to_arg_table(argument_table)


class CliInputJSONArgument(OverrideRequiredArgsArgument):
"""This argument inputs a JSON string as the entire input for a command.
Ideally, the value to this argument should be a filled out JSON file
generated by ``--generate-cli-skeleton``. The items in the JSON string
will not clobber other arguments entered into the command line.
"""
ARG_DATA = {
'name': 'cli-input-json',
'help_text': 'Performs service operation based on the JSON string '
'provided. The JSON string follows the format provided '
'by ``--generate-cli-skeleton``. If other arguments are '
'provided on the command line, it will not clobber their '
'values.'
}

def __init__(self, operation_object):
self._operation_object = operation_object
super(CliInputJSONArgument, self).__init__(
self._operation_object.session)

def _register_argument_action(self):
self._operation_object.session.register(
'calling-command', self.add_to_call_parameters)
super(CliInputJSONArgument, self)._register_argument_action()

def add_to_call_parameters(self, call_parameters, parsed_args,
parsed_globals, **kwargs):

# Check if ``--cli-input-json`` was specified in the command line.
input_json = getattr(parsed_args, 'cli_input_json', None)
if input_json is not None:
# Retrieve the JSON from the file if needed.
retrieved_json = get_paramfile(input_json)
# Nothing was retrieved from the file. So assume the argument
# is already a JSON string.
if retrieved_json is None:
retrieved_json = input_json
try:
# Try to load the JSON string into a python dictionary
input_data = json.loads(retrieved_json)
except ValueError as e:
raise ParamError(
self.name, "Invalid JSON: %s\nJSON received: %s"
% (e, retrieved_json))
# Add the members from the input JSON to the call parameters.
self._update_call_parameters(call_parameters, input_data)

def _update_call_parameters(self, call_parameters, input_data):
for input_key in input_data.keys():
# Only add the values to ``call_parameters`` if not already
# present.
if input_key not in call_parameters:
call_parameters[input_key] = input_data[input_key]
6 changes: 3 additions & 3 deletions awscli/customizations/cloudsearchdomain.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
"""

def register_cloudsearchdomain(cli):
cli.register('operation-args-parsed.cloudsearchdomain',
validate_endpoint_url)
cli.register_last('calling-command.cloudsearchdomain',
validate_endpoint_url)


def validate_endpoint_url(parsed_globals, **kwargs):
if parsed_globals.endpoint_url is None:
raise ValueError(
return ValueError(
"--endpoint-url is required for cloudsearchdomain commands")
7 changes: 6 additions & 1 deletion awscli/customizations/ec2addcount.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ def __init__(self, operation, name):
self.argument_model = model.Shape('CountArgument', {'type': 'string'})
self._operation = operation
self._name = name
self._required = False

@property
def cli_name(self):
Expand All @@ -51,7 +52,11 @@ def cli_type_name(self):

@property
def required(self):
return False
return self._required

@required.setter
def required(self, value):
self._required = value

@property
def documentation(self):
Expand Down
7 changes: 6 additions & 1 deletion awscli/customizations/ec2decryptpassword.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,19 @@ def __init__(self, operation, name):
self._operation = operation
self._name = name
self._key_path = None
self._required = False

@property
def cli_type_name(self):
return 'string'

@property
def required(self):
return False
return self._required

@required.setter
def required(self, value):
self._required = value

@property
def documentation(self):
Expand Down
87 changes: 87 additions & 0 deletions awscli/customizations/generatecliskeleton.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"). You
# may not use this file except in compliance with the License. A copy of
# the License is located at
#
# http://aws.amazon.com/apache2.0/
#
# or in the "license" file accompanying this file. This file is
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
# ANY KIND, either express or implied. See the License for the specific
# language governing permissions and limitations under the License.
import json
import sys

from botocore.utils import ArgumentGenerator

from awscli.customizations.arguments import OverrideRequiredArgsArgument


def register_generate_cli_skeleton(cli):
cli.register('building-argument-table', add_generate_skeleton)


def add_generate_skeleton(operation, argument_table, **kwargs):
# This argument cannot support operations with streaming output which
# is designated by the argument name `outfile`.
if 'outfile' not in argument_table:
generate_cli_skeleton_argument = GenerateCliSkeletonArgument(operation)
generate_cli_skeleton_argument.add_to_arg_table(argument_table)


class GenerateCliSkeletonArgument(OverrideRequiredArgsArgument):
"""This argument writes a generated JSON skeleton to stdout
The argument, if present in the command line, will prevent the intended
command from taking place. Instead, it will generate a JSON skeleton and
print it to standard output. This JSON skeleton then can be filled out
and can be used as input to ``--input-cli-json`` in order to run the
command with the filled out JSON skeleton.
"""
ARG_DATA = {
'name': 'generate-cli-skeleton',
'help_text': 'Prints a sample input JSON to standard output. Note the '
'specified operation is not run if this argument is '
'specified. The sample input can be used as an argument '
'for ``--cli-input-json``.',
'action': 'store_true',
'group_name': 'generate_cli_skeleton'
}

def __init__(self, operation_object):
self._operation_object = operation_object
super(GenerateCliSkeletonArgument, self).__init__(
self._operation_object.session)

def _register_argument_action(self):
self._operation_object.session.register(
'calling-command.*', self.generate_json_skeleton)
super(GenerateCliSkeletonArgument, self)._register_argument_action()

def generate_json_skeleton(self, call_parameters, parsed_args,
parsed_globals, **kwargs):

# Only perform the method if the ``--generate-cli-skeleton`` was
# included in the command line.
if getattr(parsed_args, 'generate_cli_skeleton', False):

# Obtain the model of the operation
operation_model = self._operation_object.model

# Generate the skeleton based on the ``input_shape``.
argument_generator = ArgumentGenerator()
operation_input_shape = operation_model.input_shape
# If the ``input_shape`` is ``None``, generate an empty
# dictionary.
if operation_input_shape is None:
skeleton = {}
else:
skeleton = argument_generator.generate_skeleton(
operation_input_shape)

# Write the generated skeleton to standard output.
sys.stdout.write(json.dumps(skeleton, indent=4))
sys.stdout.write('\n')
# This is the return code
return 0
7 changes: 6 additions & 1 deletion awscli/customizations/paginate.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def __init__(self, name, documentation, operation, parse_type):
self._name = name
self._documentation = documentation
self._parse_type = parse_type
self._required = False

@property
def cli_name(self):
Expand All @@ -151,7 +152,11 @@ def cli_type_name(self):

@property
def required(self):
return False
return self._required

@required.setter
def required(self, value):
self._required = value

@property
def documentation(self):
Expand Down
Loading

0 comments on commit 987c47c

Please sign in to comment.