Repository: ifosch/accloudtant

Issue:
CSV as output format
The table format is great to see the data in the terminal, but to facilytate the import into other tools, CSV is a best (and simple) choice.

Comments:

Patch:
diff --git a/accloudtant/aws/reports.py b/accloudtant/aws/reports.py
index 0f56ea5..6754f6b 100644
--- a/accloudtant/aws/reports.py
+++ b/accloudtant/aws/reports.py
@@ -22,15 +22,15 @@ from accloudtant.aws.reserved_instance import ReservedInstance
 from accloudtant.aws.prices import Prices
 import sys
 
-
 class Reports(object):
-    def __init__(self, logger=None):
+    def __init__(self, output_format, logger=None):
         if logger is None:
             self.logger = getLogger('accloudtant.report')
             self.logger.setLevel(DEBUG)
             self.logger.addHandler(StreamHandler(sys.stdout))
         else:
             self.logger = logger
+        self.output_format=output_format
         ec2 = boto3.resource('ec2')
         ec2_client = boto3.client('ec2')
         self.counters = {
@@ -91,7 +91,7 @@ class Reports(object):
         reserved_counters['not reserved'] = instances_counters['running']
         reserved_counters['not reserved'] -= reserved_counters['used']
 
-    def __repr__(self):
+    def print_report(self):
         headers = [
             'Id',
             'Name',
@@ -119,24 +119,45 @@ class Reports(object):
                 instance.best,
             ]
             table.append(row)
-        footer_headers = [
-            'Running',
-            'Stopped',
-            'Total instances',
-            'Used',
-            'Free',
-            'Not reserved',
-            'Total reserved',
-        ]
-        footer_table = [[
-            self.counters['instances']['running'],
-            self.counters['instances']['stopped'],
-            self.counters['instances']['total'],
-            self.counters['reserved']['used'],
-            self.counters['reserved']['free'],
-            self.counters['reserved']['not reserved'],
-            self.counters['reserved']['total'],
-        ]]
-        inventory = tabulate(table, headers)
-        summary = tabulate(footer_table, footer_headers)
-        return "{}\n\n{}".format(inventory, summary)
+
+        if self.output_format == 'table':
+            footer_headers = [
+                'Running',
+                'Stopped',
+                'Total instances',
+                'Used',
+                'Free',
+                'Not reserved',
+                'Total reserved',
+            ]
+            footer_table = [[
+                self.counters['instances']['running'],
+                self.counters['instances']['stopped'],
+                self.counters['instances']['total'],
+                self.counters['reserved']['used'],
+                self.counters['reserved']['free'],
+                self.counters['reserved']['not reserved'],
+                self.counters['reserved']['total'],
+            ]]
+            inventory = tabulate(table, headers)
+            summary = tabulate(footer_table, footer_headers)
+
+            return "{}\n\n{}".format(inventory, summary)
+
+        elif self.output_format == 'csv':
+            output = ''
+            for header in headers:
+                output += header + ','
+            output = output[:-1] + '\n'
+            for row in table:
+                for column in row:
+                    output += str(column) + ','
+                output = output[:-1] + '\n'
+
+            return output[:-1]
+
+        else:
+            raise Exception()
+
+    def __repr__(self):
+        return self.print_report()
diff --git a/bin/accloudtant b/bin/accloudtant
index 148bc66..f9ab2b0 100755
--- a/bin/accloudtant
+++ b/bin/accloudtant
@@ -14,15 +14,15 @@ logger.addHandler(StreamHandler(sys.stdout))
 def cli():
     pass
 
-
 @cli.command('list', short_help='prints current price lists')
 def price_list():
     logger.info(Prices())
 
-
 @cli.command(short_help='provides price/usage reports')
-def report():
-    logger.info(Reports())
+@click.option('--output', default='table', type=click.Choice(['table', 'csv']),
+              help='Change output format')
+def report(output):
+    logger.info(Reports(output_format=output))
 
 if __name__ == '__main__':
     cli()


Test patch:
diff --git a/tests/aws/report_running_expected_csv.txt b/tests/aws/report_running_expected_csv.txt
new file mode 100644
index 0000000..ba4cbe8
--- /dev/null
+++ b/tests/aws/report_running_expected_csv.txt
@@ -0,0 +1,7 @@
+Id,Name,Type,AZ,OS,State,Launch time,Reserved,Current hourly price,Renewed hourly price
+i-912a4392,web1,c3.8xlarge,us-east-1c,Windows,running,2015-10-22 14:15:10,Yes,0.5121,0.3894
+i-1840273e,app1,r2.8xlarge,us-east-1b,Red Hat Enterprise Linux,running,2015-10-22 14:15:10,Yes,0.3894,0.3794
+i-9840273d,app2,r2.8xlarge,us-east-1c,SUSE Linux,running,2015-10-22 14:15:10,Yes,0.5225,0.389
+i-1840273c,database2,r2.8xlarge,us-east-1c,Linux/UNIX,running,2015-10-22 14:15:10,Yes,0.611,0.379
+i-1840273b,database3,r2.8xlarge,us-east-1c,Linux/UNIX,running,2015-10-22 14:15:10,Yes,0.611,0.379
+i-912a4393,test,t1.micro,us-east-1c,Linux/UNIX,running,2015-10-22 14:15:10,No,0.767,0.3892
diff --git a/tests/aws/report_running_expected.txt b/tests/aws/report_running_expected_table.txt
similarity index 100%
rename from tests/aws/report_running_expected.txt
rename to tests/aws/report_running_expected_table.txt
diff --git a/tests/aws/test_reports.py b/tests/aws/test_reports.py
index 57084c3..80d2c24 100644
--- a/tests/aws/test_reports.py
+++ b/tests/aws/test_reports.py
@@ -20,8 +20,635 @@ import accloudtant.aws.reports
 def get_future_date(years=1):
     return datetime.datetime.now() + datetime.timedelta(years)
 
+def test_reports_table(capsys, monkeypatch, ec2_resource, ec2_client,
+                       process_ec2):
+    instances = {
+        'instances': [{
+            'id': 'i-912a4392',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'web1',
+            }, ],
+            'instance_type': 'c3.8xlarge',
+            'placement': {
+                'AvailabilityZone': 'us-east-1c',
+            },
+            'state': {
+                'Name': 'running',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc(),
+                ),
+            'console_output': {'Output': 'Windows', },
+        }, {
+            'id': 'i-1840273e',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'app1',
+            }, ],
+            'instance_type': 'r2.8xlarge',
+            'placement': {
+                'AvailabilityZone': 'us-east-1b',
+            },
+            'state': {
+                'Name': 'running',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'console_output': {'Output': 'RHEL Linux', },
+        }, {
+            'id': 'i-9840273d',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'app2',
+            }, ],
+            'instance_type': 'r2.8xlarge',
+            'placement': {
+                'AvailabilityZone': 'us-east-1c',
+            },
+            'state': {
+                'Name': 'running',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'console_output': {'Output': 'SUSE Linux', },
+        }, {
+            'id': 'i-1840273d',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'database1',
+            }, ],
+            'instance_type': 'r2.8xlarge',
+            'placement': {
+                'AvailabilityZone': 'us-east-1c',
+            },
+            'state': {
+                'Name': 'stopped',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'console_output': {'Output': 'Linux', },
+        }, {
+            'id': 'i-1840273c',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'database2',
+            }, ],
+            'instance_type': 'r2.8xlarge',
+            'placement': {
+                'AvailabilityZone': 'us-east-1c',
+            },
+            'state': {
+                'Name': 'running',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'console_output': {'Output': 'Linux', },
+        }, {
+            'id': 'i-1840273b',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'database3',
+            }, ],
+            'instance_type': 'r2.8xlarge',
+            'placement': {
+                'AvailabilityZone': 'us-east-1c',
+            },
+            'state': {
+                'Name': 'running',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'console_output': {'Output': 'Linux', },
+        }, {
+            'id': 'i-912a4393',
+            'tags': [{
+                'Key': 'Name',
+                'Value': 'test',
+            }, ],
+            'instance_type': 't1.micro',
+            'placement': {
+                'AvailabilityZone': 'us-east-1c',
+            },
+            'state': {
+                'Name': 'running',
+            },
+            'launch_time': datetime.datetime(
+                    2015,
+                    10,
+                    22,
+                    14,
+                    15,
+                    10,
+                    tzinfo=tzutc(),
+                ),
+            'console_output': {'Output': 'Linux', },
+        }, ]
+    }
+    reserved_instances = {
+        'ReservedInstances': [{
+            'ProductDescription': 'Linux/UNIX',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 29,
+            'InstanceType': 'm1.large',
+            'Start': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df1223331f',
+            'FixedPrice': 910.0,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.12,
+            'Duration': 31536000,
+            'State': 'retired',
+        }, {
+            'ProductDescription': 'Windows',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 'c3.8xlarge',
+            'Start': datetime.datetime(
+                    2015,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': get_future_date(),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233320',
+            'FixedPrice': 4486.0,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.5121,
+            'Duration': 31536000,
+            'State': 'active',
+        }, {
+            'ProductDescription': 'Red Hat Enterprise Linux',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 'r2.8xlarge',
+            'Start': datetime.datetime(
+                    2015,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': get_future_date(),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233321',
+            'FixedPrice': 10234,
+            'AvailabilityZone': 'us-east-1b',
+            'UsagePrice': 0.3894,
+            'Duration': 94608000,
+            'State': 'active',
+        }, {
+            'ProductDescription': 'SUSE Linux',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 'r2.8xlarge',
+            'Start': datetime.datetime(
+                    2015,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': get_future_date(),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233322',
+            'FixedPrice': 2974.0,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.5225,
+            'Duration': 31536000,
+            'State': 'active',
+        }, {
+            'ProductDescription': 'Linux/UNIX',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 'r2.8xlarge',
+            'Start': datetime.datetime(
+                    2015,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': get_future_date(),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233320',
+            'FixedPrice': 5352.36,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.611,
+            'Duration': 31536000,
+            'State': 'active',
+        }, {
+            'ProductDescription': 'Linux/UNIX',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 't2.micro',
+            'Start': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233320',
+            'FixedPrice': 5352.36,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.611,
+            'Duration': 31536000,
+            'State': 'active',
+        }, {
+            'ProductDescription': 'Linux/UNIX',
+            'InstanceTenancy': 'default',
+            'InstanceCount': 1,
+            'InstanceType': 'r2.8xlarge',
+            'Start': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    494000,
+                    tzinfo=tzutc()
+                ),
+            'RecurringCharges': [],
+            'End': datetime.datetime(
+                    2011,
+                    6,
+                    5,
+                    6,
+                    20,
+                    10,
+                    tzinfo=tzutc()
+                ),
+            'CurrencyCode': 'USD',
+            'OfferingType': 'Medium Utilization',
+            'ReservedInstancesId': '46a408c7-c33d-422d-af59-28df12233320',
+            'FixedPrice': 5352.36,
+            'AvailabilityZone': 'us-east-1c',
+            'UsagePrice': 0.611,
+            'Duration': 31536000,
+            'State': 'active',
+        }, ]
+    }
+    prices = {
+        'win': {
+            'us-east-1': {
+                'c3.8xlarge': {
+                    'storageGB': '60 SSD',
+                    'ri': {
+                        'yrTerm1': {
+                            'noUpfront': {
+                                'upfront': '0',
+                                'monthlyStar': '446.03',
+                                'effectiveHourly': '0.611',
+                            },
+                            'partialUpfront': {
+                                'upfront': '2974',
+                                'monthlyStar': '133.59',
+                                'effectiveHourly': '0.5225',
+                            },
+                            'allUpfront': {
+                                'upfront': '4398.4',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.5021',
+                            },
+                        },
+                        'yrTerm3': {
+                            'allUpfront': {
+                                'upfront': '10234',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.3894',
+                            },
+                            'partialUpfront': {
+                                'upfront': '7077',
+                                'monthlyStar': '105.85',
+                                'effectiveHourly': '0.4143',
+                            },
+                        },
+                    },
+                    'od': '0.867',
+                    'memoryGiB': '15',
+                    'vCPU': '8',
+                },
+            },
+        },
+        'rhel': {
+            'us-east-1': {
+                'r2.8xlarge': {
+                    'storageGB': '60 SSD',
+                    'ri': {
+                        'yrTerm1': {
+                            'noUpfront': {
+                                'upfront': '0',
+                                'monthlyStar': '446.03',
+                                'effectiveHourly': '0.611',
+                            },
+                            'partialUpfront': {
+                                'upfront': '2974',
+                                'monthlyStar': '133.59',
+                                'effectiveHourly': '0.5225',
+                            },
+                            'allUpfront': {
+                                'upfront': '4486',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.5121',
+                            },
+                        },
+                        'yrTerm3': {
+                            'allUpfront': {
+                                'upfront': '10233.432',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.3794',
+                            },
+                            'partialUpfront': {
+                                'upfront': '7077',
+                                'monthlyStar': '105.85',
+                                'effectiveHourly': '0.4143',
+                            },
+                        },
+                    },
+                    'od': '0.767',
+                    'memoryGiB': '15',
+                    'vCPU': '8',
+                },
+            },
+        },
+        'suse': {
+            'us-east-1': {
+                'r2.8xlarge': {
+                    'storageGB': '60 SSD',
+                    'ri': {
+                        'yrTerm1': {
+                            'noUpfront': {
+                                'upfront': '0',
+                                'monthlyStar': '446.03',
+                                'effectiveHourly': '0.611',
+                            },
+                            'partialUpfront': {
+                                'upfront': '2974',
+                                'monthlyStar': '133.59',
+                                'effectiveHourly': '0.5225',
+                            },
+                            'allUpfront': {
+                                'upfront': '4486',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.5121',
+                            },
+                        },
+                        'yrTerm3': {
+                            'allUpfront': {
+                                'upfront': '10234',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.3890',
+                            },
+                            'partialUpfront': {
+                                'upfront': '7077',
+                                'monthlyStar': '105.85',
+                                'effectiveHourly': '0.4143',
+                            },
+                        },
+                    },
+                    'od': '0.767',
+                    'memoryGiB': '15',
+                    'vCPU': '8',
+                },
+            },
+        },
+        'linux': {
+            'us-east-1': {
+                't1.micro': {
+                    'storageGB': '60 SSD',
+                    'ri': {
+                        'yrTerm1': {
+                            'noUpfront': {
+                                'upfront': '0',
+                                'monthlyStar': '446.03',
+                                'effectiveHourly': '0.611',
+                            },
+                            'allUpfront': {
+                                'upfront': '2974',
+                                'monthlyStar': '133.59',
+                                'effectiveHourly': '0.5225',
+                            },
+                            'partialUpfront': {
+                                'upfront': '4486',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.5121',
+                            },
+                        },
+                        'yrTerm3': {
+                            'allUpfront': {
+                                'upfront': '10234',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.3892',
+                            },
+                            'partialUpfront': {
+                                'upfront': '7077',
+                                'monthlyStar': '105.85',
+                                'effectiveHourly': '0.4143',
+                            },
+                        },
+                    },
+                    'od': '0.767',
+                    'memoryGiB': '15',
+                    'vCPU': '8',
+                },
+                'r2.8xlarge': {
+                    'storageGB': '60 SSD',
+                    'ri': {
+                        'yrTerm1': {
+                            'noUpfront': {
+                                'upfront': '0',
+                                'monthlyStar': '446.03',
+                                'effectiveHourly': '0.611',
+                            },
+                            'allUpfront': {
+                                'upfront': '2974',
+                                'monthlyStar': '133.59',
+                                'effectiveHourly': '0.5225',
+                            },
+                            'partialUpfront': {
+                                'upfront': '4486',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.5121',
+                            },
+                        },
+                        'yrTerm3': {
+                            'allUpfront': {
+                                'upfront': '10234',
+                                'monthlyStar': '0',
+                                'effectiveHourly': '0.3790',
+                            },
+                            'partialUpfront': {
+                                'upfront': '7077',
+                                'monthlyStar': '105.85',
+                                'effectiveHourly': '0.4143',
+                            },
+                        },
+                    },
+                    'od': '0.767',
+                    'memoryGiB': '15',
+                    'vCPU': '8',
+                },
+            },
+        },
+    }
+    instances_prices = {
+        'i-912a4392': {
+            'current': 0.5121,
+            'best': 0.3894,
+        },
+        'i-1840273e': {
+            'current': 0.3894,
+            'best': 0.3794,
+        },
+        'i-9840273d': {
+            'current': 0.5225,
+            'best': 0.3890,
+        },
+        'i-1840273d': {
+            'current': 0.0,
+            'best': 0.3790,
+        },
+        'i-1840273c': {
+            'current': 0.611,
+            'best': 0.3790,
+        },
+        'i-1840273b': {
+            'current': 0.611,
+            'best': 0.3790,
+        },
+        'i-912a4393': {
+            'current': 0.767,
+            'best': 0.3892,
+        },
+    }
+    expected = open('tests/aws/report_running_expected_table.txt', 'r').read()
+
+    monkeypatch.setattr('boto3.resource', ec2_resource)
+    ec2_resource.set_responses(instances)
+    monkeypatch.setattr('boto3.client', ec2_client)
+    ec2_client.set_responses({}, reserved_instances)
+    monkeypatch.setattr(
+        'accloudtant.aws.prices.process_ec2',
+        process_ec2
+        )
+    process_ec2.set_responses(prices)
+
+    reports = accloudtant.aws.reports.Reports(output_format='table')
+    print(reports)
+    out, err = capsys.readouterr()
+
+    assert(len(reports.instances) == 6)
+    for mock in instances['instances']:
+        mock['current'] = instances_prices[mock['id']]['current']
+        mock['best'] = instances_prices[mock['id']]['best']
+        for instance in reports.instances:
+            if instance.id == mock['id']:
+                assert(instance.current == mock['current'])
+                assert(instance.best == mock['best'])
+    assert(out == expected)
 
-def test_reports(capsys, monkeypatch, ec2_resource, ec2_client, process_ec2):
+def test_reports_csv(capsys, monkeypatch, ec2_resource, ec2_client,
+                       process_ec2):
     instances = {
         'instances': [{
             'id': 'i-912a4392',
@@ -621,7 +1248,7 @@ def test_reports(capsys, monkeypatch, ec2_resource, ec2_client, process_ec2):
             'best': 0.3892,
         },
     }
-    expected = open('tests/aws/report_running_expected.txt', 'r').read()
+    expected = open('tests/aws/report_running_expected_csv.txt', 'r').read()
 
     monkeypatch.setattr('boto3.resource', ec2_resource)
     ec2_resource.set_responses(instances)
@@ -633,7 +1260,7 @@ def test_reports(capsys, monkeypatch, ec2_resource, ec2_client, process_ec2):
         )
     process_ec2.set_responses(prices)
 
-    reports = accloudtant.aws.reports.Reports()
+    reports = accloudtant.aws.reports.Reports(output_format='csv')
     print(reports)
     out, err = capsys.readouterr()
 
@@ -646,3 +1273,4 @@ def test_reports(capsys, monkeypatch, ec2_resource, ec2_client, process_ec2):
                 assert(instance.current == mock['current'])
                 assert(instance.best == mock['best'])
     assert(out == expected)
+