Repository: aws/aws-cli

Issue:
aws cloudwatch put-metric-data no longer working with statistic sets
One of our automated scripts stopped reporting data a few weeks ago - we've traced this to a newer version of the AWS CLI.

In fact, the documented example for how to publish statistic sets (http://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/publishingMetrics.html#publishingDataPoints1) fails with the same error that we are getting.

```
$ aws cloudwatch put-metric-data --metric-name PageViewCount --namespace "MyService" --statistic-value Sum=11,Minimum=2,Maximum=5,SampleCount=3 --timestamp 2014-02-14T12:00:00.000Z

Parameter validation failed:
Invalid type for parameter MetricData[0].StatisticValues.SampleCount, value: 3, type: <type 'unicode'>, valid types: <type 'float'>, <class 'decimal.Decimal'>, <type 'int'>, <type 'long'>
Invalid type for parameter MetricData[0].StatisticValues.Sum, value: 11, type: <type 'unicode'>, valid types: <type 'float'>, <class 'decimal.Decimal'>, <type 'int'>, <type 'long'>
Invalid type for parameter MetricData[0].StatisticValues.Minimum, value: 2, type: <type 'unicode'>, valid types: <type 'float'>, <class 'decimal.Decimal'>, <type 'int'>, <type 'long'>
Invalid type for parameter MetricData[0].StatisticValues.Maximum, value: 5, type: <type 'unicode'>, valid types: <type 'float'>, <class 'decimal.Decimal'>, <type 'int'>, <type 'long'>
```



Discussion:
Working on a fix.


Resolving patch:
diff --git a/awscli/customizations/putmetricdata.py b/awscli/customizations/putmetricdata.py
--- a/awscli/customizations/putmetricdata.py
+++ b/awscli/customizations/putmetricdata.py
@@ -98,24 +98,24 @@ def add_to_params(self, parameters, value):
         method_name = '_add_param_%s' % self.name.replace('-', '_')
         return getattr(self, method_name)(parameters, value)
 
-    @insert_first_element('metric_data')
+    @insert_first_element('MetricData')
     def _add_param_metric_name(self, first_element, value):
         first_element['MetricName'] = value
 
-    @insert_first_element('metric_data')
+    @insert_first_element('MetricData')
     def _add_param_unit(self, first_element, value):
         first_element['Unit'] = value
 
-    @insert_first_element('metric_data')
+    @insert_first_element('MetricData')
     def _add_param_timestamp(self, first_element, value):
         first_element['Timestamp'] = value
 
-    @insert_first_element('metric_data')
+    @insert_first_element('MetricData')
     def _add_param_value(self, first_element, value):
         # Use a Decimal to avoid loss in precision.
         first_element['Value'] = decimal.Decimal(value)
 
-    @insert_first_element('metric_data')
+    @insert_first_element('MetricData')
     def _add_param_dimensions(self, first_element, value):
         # Dimensions needs a little more processing.  We support
         # the key=value,key2=value syntax so we need to parse
@@ -126,12 +126,15 @@ def _add_param_dimensions(self, first_element, value):
             dimensions.append({'Name': key, 'Value': value})
         first_element['Dimensions'] = dimensions
 
-    @insert_first_element('metric_data')
+    @insert_first_element('MetricData')
     def _add_param_statistic_values(self, first_element, value):
         # StatisticValues is a struct type so we are parsing
         # a csv keyval list into a dict.
         statistics = {}
         for pair in split_on_commas(value):
             key, value = pair.split('=')
-            statistics[key] = value
+            # There are four supported values: Maximum, Minimum, SampleCount,
+            # and Sum.  All of them are documented as a type double so we can
+            # convert these to a decimal value to preserve precision.
+            statistics[key] = decimal.Decimal(value)
         first_element['StatisticValues'] = statistics


Test patch:
diff --git a/tests/unit/cloudwatch/test_put_metric_data.py b/tests/unit/cloudwatch/test_put_metric_data.py
--- a/tests/unit/cloudwatch/test_put_metric_data.py
+++ b/tests/unit/cloudwatch/test_put_metric_data.py
@@ -11,6 +11,8 @@
 # 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 decimal
+
 from awscli.testutils import BaseAWSCommandParamsTest
 
 
@@ -20,12 +22,12 @@ class TestPutMetricData(BaseAWSCommandParamsTest):
     prefix = 'cloudwatch put-metric-data '
 
     expected_output = {
-        'MetricData.member.1.MetricName': 'FreeMemoryBytes',
-        'MetricData.member.1.Timestamp': '2013-08-22T10:58:12.283000Z',
-        'MetricData.member.1.Unit': 'Bytes',
-        'MetricData.member.1.Value': 9130160128,
-        'Namespace': '"Foo/Bar"'
-    }
+        'MetricData': [
+            {'MetricName': 'FreeMemoryBytes',
+             'Unit': 'Bytes',
+             'Timestamp': '2013-08-22T10:58:12.283Z',
+             'Value': decimal.Decimal("9130160128")}],
+        'Namespace': '"Foo/Bar"'}
 
     def test_using_json(self):
         args = ('--namespace "Foo/Bar" '
@@ -34,7 +36,7 @@ def test_using_json(self):
                 '"Timestamp":"2013-08-22T10:58:12.283Z",'
                 '"Value":9130160128}]')
         cmdline = self.prefix + args
-        self.assert_params_for_cmd(cmdline, self.expected_output)
+        self.assert_params_for_cmd2(cmdline, self.expected_output)
 
     def test_using_promoted_params(self):
         # This is equivalent to the json version in test_using_json
@@ -45,4 +47,26 @@ def test_using_promoted_params(self):
                 '--timestamp 2013-08-22T10:58:12.283Z '
                 '--value 9130160128')
         cmdline = self.prefix + args
-        self.assert_params_for_cmd(cmdline, self.expected_output)
+        self.assert_params_for_cmd2(cmdline, self.expected_output)
+
+    def test_using_shorthand_syntax(self):
+        args = (
+            '--metric-name PageViewCount '
+            '--namespace MyService '
+            '--statistic-value Sum=11,Minimum=2,Maximum=5,SampleCount=3 '
+            '--timestamp 2014-02-14T12:00:00.000Z'
+        )
+        cmdline = self.prefix + args
+        expected = {
+            'MetricData': [
+                {'MetricName': 'PageViewCount',
+                 'StatisticValues': {
+                     'Maximum': decimal.Decimal('5'),
+                     'Minimum': decimal.Decimal('2'),
+                     'SampleCount': decimal.Decimal('3'),
+                     'Sum': decimal.Decimal('11')},
+                 'Timestamp': '2014-02-14T12:00:00.000Z'}
+            ],
+            'Namespace': 'MyService'
+        }
+        self.assert_params_for_cmd2(cmdline, expected)
diff --git a/tests/unit/customizations/test_cloudwatch.py b/tests/unit/customizations/test_cloudwatch.py
--- a/tests/unit/customizations/test_cloudwatch.py
+++ b/tests/unit/customizations/test_cloudwatch.py
@@ -25,7 +25,7 @@ def test_build_metric_name_arg(self):
                                               help_text='metric-name')
         parameters = {}
         arg.add_to_params(parameters, 'MyMetricName')
-        self.assertEqual(parameters['metric_data'][0]['MetricName'],
+        self.assertEqual(parameters['MetricData'][0]['MetricName'],
                          'MyMetricName')
 
     def test_build_unit_arg(self):
@@ -33,7 +33,7 @@ def test_build_unit_arg(self):
                                               help_text='unit')
         parameters = {}
         arg.add_to_params(parameters, 'Percent')
-        self.assertEqual(parameters['metric_data'][0]['Unit'],
+        self.assertEqual(parameters['MetricData'][0]['Unit'],
                          'Percent')
 
     def test_value_arg(self):
@@ -41,7 +41,7 @@ def test_value_arg(self):
                                               help_text='value')
         parameters = {}
         arg.add_to_params(parameters, '123.1')
-        self.assertEqual(parameters['metric_data'][0]['Value'],
+        self.assertEqual(parameters['MetricData'][0]['Value'],
                          decimal.Decimal('123.1'))
 
     def test_timestamp_arg(self):
@@ -49,7 +49,7 @@ def test_timestamp_arg(self):
                                               help_text='timestamp')
         parameters = {}
         arg.add_to_params(parameters, '2013-09-01')
-        self.assertEqual(parameters['metric_data'][0]['Timestamp'],
+        self.assertEqual(parameters['MetricData'][0]['Timestamp'],
                          '2013-09-01')
 
     def test_dimensions_arg(self):
@@ -57,7 +57,7 @@ def test_dimensions_arg(self):
                                               help_text='dimensions')
         parameters = {}
         arg.add_to_params(parameters, 'User=someuser,Stack=test')
-        self.assertEqual(parameters['metric_data'][0]['Dimensions'],
+        self.assertEqual(parameters['MetricData'][0]['Dimensions'],
                          [{"Name": "User", "Value": "someuser"},
                           {"Name": "Stack", "Value": "test"}])
 
@@ -67,9 +67,11 @@ def test_statistics_arg(self):
         parameters = {}
         arg.add_to_params(parameters,
                           'Sum=250,Minimum=30,Maximum=70,SampleCount=5')
-        self.assertEqual(parameters['metric_data'][0]['StatisticValues'],
-                         {'Maximum': '70', 'Minimum': '30',
-                          'SampleCount': '5', 'Sum': '250'})
+        self.assertEqual(parameters['MetricData'][0]['StatisticValues'],
+                         {'Maximum': decimal.Decimal('70'),
+                          'Minimum': decimal.Decimal('30'),
+                          'SampleCount': decimal.Decimal('5'),
+                          'Sum': decimal.Decimal('250')})
 
     def test_parse_empty_value(self):
         arg = putmetricdata.PutMetricArgument('dimensions',