├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── details ├── __init__.py ├── _version └── costs.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── unit ├── __init__.py ├── data └── detailed_with_tags.csv └── test_costs.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | # Emacs backup files 57 | *~ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.3" 5 | - "3.4" 6 | install: 7 | - pip install -r requirements.txt 8 | - pip install coverage python-coveralls 9 | script: nosetests tests/unit --cover-erase --with-coverage --cover-package details 10 | after_success: coveralls 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE 3 | include requirements.txt 4 | include details/_version 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/scopely-devops/details.svg?branch=master)](https://travis-ci.org/scopely-devops/details) 2 | 3 | details 4 | ======= 5 | 6 | Utilities to process AWS detailed billing reports. 7 | 8 | Installation 9 | ------------ 10 | 11 | The easiest way to install ``details`` is by using ``pip``: 12 | 13 | $ pip install details 14 | 15 | 16 | Backgroud 17 | --------- 18 | 19 | The ``details`` package parses the detailed billing reports produced by 20 | AWS. You can find out more about these reports and how to enable them 21 | for your AWS accounts [here](http://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/detailed-billing-reports.html). All testing thus far has been 22 | done using the **Detailed billing report with resources and tags** but the 23 | code should work with any detailed billing report. 24 | 25 | Once you have enabled detailed billing on your accounts, AWS will begin to 26 | save these CSV reports in the S3 bucket you have configured. The ``details`` 27 | library assumes you have copied the reports from S3 to your local file 28 | system and uncompressed them if necessary. The [AWS CLI](https://aws.amazon.com/cli) is a good way to copy the files from S3. 29 | 30 | The detailed billing files contain a row (a line item) for every charge on an 31 | account with hourly granularity. These files can get **huge**. These tools 32 | currently load an entire months worth of data into memory so if you have a 33 | really large bill this could become impractical. However, it has been 34 | demonstrated to work reasonably well on detailed billing reports containing 35 | millions of line item records. 36 | 37 | Usage 38 | ----- 39 | 40 | Once you have a detailed billing CSV file available locally, you can load 41 | the file into ``details`` like this: 42 | 43 | >>> import details 44 | >>> total = details.load('../../bills/123456789012-aws-billing-detailed...') 45 | 46 | The variable ``costs`` now points to a ``Cost`` object which contains all of 47 | the line item data for the entire billing file. Note that depending on the 48 | size of your detailed billing report, this operation can take some time. 49 | 50 | Now that you have the ``Cost`` object, you can start by asking it for 51 | the total cost of the detailed billing report: 52 | 53 | >>> total.cost 54 | Decimal('1035.7549984289') 55 | >>> 56 | 57 | This number should match (or very nearly match, there are some rounding 58 | errors at times) the total on your bill. The other thing to note is that 59 | the value returned is a Python Decimal number. The Decimal type is used 60 | to avoid any further rounding errors within the ``details`` package. 61 | You can use these Decimal numbers as you would normal ints or floats. 62 | Checkout [this](https://docs.python.org/2/library/decimal.html) 63 | for more details on the Decimal type. 64 | 65 | In addition to telling you the total cost, the ``Cost`` object has 66 | a few other useful methods. 67 | 68 | To find all of the *columns* in CSV data: 69 | 70 | >>> total.columns 71 | ['InvoiceID', 72 | 'PayerAccountId', 73 | 'LinkedAccountId', 74 | 'RecordType', 75 | 'RecordId', 76 | 'ProductName', 77 | 'RateId', 78 | 'SubscriptionId', 79 | 'PricingPlanId', 80 | 'UsageType', 81 | 'Operation', 82 | 'AvailabilityZone', 83 | 'ReservedInstance', 84 | 'ItemDescription', 85 | 'UsageStartDate', 86 | 'UsageEndDate', 87 | 'UsageQuantity', 88 | 'BlendedRate', 89 | 'BlendedCost', 90 | 'UnBlendedRate', 91 | 'UnBlendedCost', 92 | 'ResourceId', 93 | 'user:Name', 94 | 'user:Role'] 95 | 96 | To find all possible values found within a particular column: 97 | 98 | >>> total.values('ProductName') 99 | ['Amazon Simple Storage Service', 100 | 'Amazon DynamoDB', 101 | 'Amazon Route 53', 102 | 'Route 53 Domain Registration Service', 103 | 'AWS Data Pipeline', 104 | 'Amazon Elastic MapReduce', 105 | 'Amazon RDS Service', 106 | 'Amazon Zocalo', 107 | 'Amazon Simple Queue Service', 108 | 'AWS Support (Business)', 109 | 'Amazon Simple Notification Service', 110 | 'Amazon CloudFront', 111 | 'AWS Support (Developer)', 112 | 'Amazon WorkSpaces', 113 | 'Amazon Redshift', 114 | 'Amazon Elastic Compute Cloud', 115 | 'Amazon ElastiCache', 116 | 'Amazon Kinesis', 117 | 'Amazon CloudSearch', 118 | 'Amazon SimpleDB', 119 | 'Amazon Simple Email Service'] 120 | 121 | This list will include only the services that actually were used within 122 | this account. 123 | 124 | If this report was for a consolidated account, you could find all of the 125 | accounts contained within this report like this: 126 | 127 | >>> total.values('LinkedAccountId') 128 | ['012345678901', 129 | '123456789012'] 130 | >>> 131 | 132 | The above total represents all of the costs for all services within this 133 | account. What if you wanted to find the costs just for the EC2 service? 134 | To do this, use the ``filter`` method. It takes a list of filters where 135 | each filter consists of a column name and a regular expression. Each value in 136 | that column name is compared to the regular expression and if it matches 137 | it is collected and returned with all of the other matches in another 138 | ``Costs`` object. 139 | 140 | >>> ec2 = total.filter([('ProductName', '.*Compute Cloud.*')]) 141 | >>> ec2.cost 142 | Decimal('315.88505363') 143 | >>> 144 | 145 | So the total cost for EC2 in this account was $315.89. However, if you 146 | compare that to your bill you will probably find that it doesn't match. 147 | The reason (probably) is that this number includes all Data Transfer 148 | charges incurred as part of the EC2 usage but your monthly bill breaks 149 | data transfer out as a separate line item. To break out the data transfer 150 | costs: 151 | 152 | >>> data_transfer = ec2.filter([('UsageType', '.*DataTransfer.*')]) 153 | >>> data_transfer.cost 154 | Decimal('35.45916220') 155 | >>> 156 | 157 | So, if you subtract the data transfer costs from the EC2 costs you should 158 | see the number on your bill. 159 | -------------------------------------------------------------------------------- /details/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Scopely, Inc. 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 | import csv 14 | import decimal 15 | 16 | from details.costs import Costs 17 | 18 | 19 | def load(file): 20 | """ 21 | This function expects a path to a file containing a 22 | **Detailed billing report with resources and tags** 23 | report from AWS. 24 | 25 | It returns a ``Costs`` object containing all of the lineitems 26 | from that detailed billing report 27 | """ 28 | fp = open(file) 29 | reader = csv.reader(fp) 30 | headers = next(reader) 31 | costs = Costs(headers) 32 | for line in reader: 33 | data = {} 34 | for i in range(0, len(headers)): 35 | data[headers[i]] = line[i] 36 | data['UnBlendedCost'] = decimal.Decimal(data['UnBlendedCost']) 37 | data['BlendedCost'] = decimal.Decimal(data['BlendedCost']) 38 | costs.add(data) 39 | fp.close() 40 | return costs 41 | -------------------------------------------------------------------------------- /details/_version: -------------------------------------------------------------------------------- 1 | 0.2.0 -------------------------------------------------------------------------------- /details/costs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Scopely, Inc. 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 | import re 14 | 15 | """ 16 | This assumes the headers of the CSV file are in the following order: 17 | 18 | * InvoiceID 19 | * PayerAccountId 20 | * LinkedAccountId 21 | * RecordType 22 | * RecordId 23 | * ProductName 24 | * RateId 25 | * SubscriptionId 26 | * PricingPlanId 27 | * UsageType 28 | * Operation 29 | * AvailabilityZone 30 | * ReservedInstance 31 | * ItemDescription 32 | * UsageStartDate 33 | * UsageEndDate 34 | * UsageQuantity 35 | * BlendedRate 36 | * BlendedCost 37 | * UnBlendedRate 38 | * UnBlendedCost 39 | * ResourceId 40 | 41 | Followed by any allocation tags in the form: 42 | 43 | * user: 44 | """ 45 | 46 | 47 | class Costs(object): 48 | 49 | def __init__(self, columns): 50 | self._columns = columns 51 | self._tags = [c for c in self._columns if c.startswith('user:')] 52 | self._lineitems = [] 53 | self._blended_cost = 0 54 | self._unblended_cost = 0 55 | self._values = {} 56 | 57 | @property 58 | def columns(self): 59 | return self._columns 60 | 61 | @property 62 | def blended_cost(self): 63 | return self._blended_cost 64 | 65 | @property 66 | def unblended_cost(self): 67 | return self._unblended_cost 68 | 69 | @property 70 | def cost(self): 71 | return self._unblended_cost 72 | 73 | def add(self, lineitem): 74 | """ 75 | Add a line item record to this Costs object. 76 | """ 77 | # Check for a ProductName in the lineitem. 78 | # If its not there, it is a subtotal line and including it 79 | # will throw the total cost calculation off. So ignore it. 80 | if lineitem['ProductName']: 81 | self._lineitems.append(lineitem) 82 | if lineitem['BlendedCost']: 83 | self._blended_cost += lineitem['BlendedCost'] 84 | if lineitem['UnBlendedCost']: 85 | self._unblended_cost += lineitem['UnBlendedCost'] 86 | 87 | def values(self, column): 88 | if column not in self._values: 89 | self._values[column] = set() 90 | for lineitem in self._lineitems: 91 | if lineitem[column]: 92 | self._values[column].add(lineitem[column]) 93 | return list(self._values[column]) 94 | 95 | def filter(self, filters): 96 | """ 97 | Pass in a list of tuples where each tuple represents one filter. 98 | The first element of the tuple is the name of the column to 99 | filter on and the second value is a regular expression which 100 | each value in that column will be compared against. If the 101 | regular expression matches the value in that column, that 102 | lineitem will be included in the new Costs object returned. 103 | 104 | Example: 105 | 106 | filters=[('ProductName', '.*DynamoDB')] 107 | 108 | This filter would find all lineitems whose ``ProductName`` 109 | column contains values that end in the string ``DynamoDB``. 110 | """ 111 | subset = Costs(self._columns) 112 | filters = [(col, re.compile(regex)) for col, regex in filters] 113 | for lineitem in self._lineitems: 114 | for filter in filters: 115 | if filter[1].search(lineitem[filter[0]]) is None: 116 | continue 117 | subset.add(lineitem) 118 | return subset 119 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.1 2 | tox==1.7.1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | import os 6 | 7 | requires = [ 8 | ] 9 | 10 | 11 | setup( 12 | name='details', 13 | version=open(os.path.join('details', '_version')).read(), 14 | description='Tools for processing AWS detailed billing reports', 15 | long_description=open('README.md').read(), 16 | author='Mitch Garnaat', 17 | author_email='mitch@scopely.com', 18 | url='https://github.com/scopely-devops/details', 19 | packages=find_packages(exclude=['tests*']), 20 | package_dir={'details': 'details'}, 21 | install_requires=requires, 22 | license=open("LICENSE").read(), 23 | classifiers=( 24 | 'Development Status :: 3 - Alpha', 25 | 'Intended Audience :: Developers', 26 | 'Intended Audience :: System Administrators', 27 | 'Natural Language :: English', 28 | 'License :: OSI Approved :: Apache Software License', 29 | 'Programming Language :: Python', 30 | 'Programming Language :: Python :: 2.6', 31 | 'Programming Language :: Python :: 2.7', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.3', 34 | 'Programming Language :: Python :: 3.4' 35 | ), 36 | ) 37 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Scopely, Inc. 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 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Scopely, Inc. 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 | -------------------------------------------------------------------------------- /tests/unit/data/detailed_with_tags.csv: -------------------------------------------------------------------------------- 1 | InvoiceID,PayerAccountId,LinkedAccountId,RecordType,RecordId,ProductName,RateId,SubscriptionId,PricingPlanId,UsageType,Operation,AvailabilityZone,ReservedInstance,ItemDescription,UsageStartDate,UsageEndDate,UsageQuantity,BlendedRate,BlendedCost,UnBlendedRate,UnBlendedCost,ResourceId,user:Name,user:Role 2 | "11111111","123456789012","095669871260","LineItem","25470304986286315974946822","Amazon Simple Queue Service","2435338","38800708","455297","APN1-Requests-Tier1","List","","N","First 1,000,000 Amazon SQS Requests per month are free","2014-09-01 00:00:00","2014-09-01 01:00:00","16.00000000","0.0000000000","0.00000000","0.0000000000","0.00000000",,, 3 | "11111111","123456789012","095669871260","LineItem","25470286539542242265377051","Amazon Elastic Compute Cloud","3158040","38802707","489936","BoxUsage","RunInstances","us-east-1b","N","$0.044 per On Demand Linux m1.small Instance Hour","2014-09-01 00:00:00","2014-09-01 01:00:00","1.00000000","0.0440000000","0.04400000","0.0440000000","0.04400000","i-c7ae0eeb",, 4 | "11111111","123456789012","095669871260","LineItem","25470286539542242265377052","Amazon Elastic Compute Cloud","3158040","38802707","489936","BoxUsage","RunInstances","us-east-1c","N","$0.044 per On Demand Linux m1.small Instance Hour","2014-09-01 00:00:00","2014-09-01 01:00:00","1.00000000","0.0440000000","0.04400000","0.0440000000","0.04400000","i-12345678",, 5 | "11111111","123456789012","095669871260","LineItem","25470360326518537103800083","Amazon Elastic Compute Cloud","913846","38802709","205266","DataTransfer-Out-Bytes","RunInstances","","N","$0.120 per GB - up to 10 TB / month data transfer out","2014-09-01 00:00:00","2014-09-01 01:00:00","0.00000159","0.11997536721028","0.00000019","0.1200000000","0.00000019","i-23456789",, 6 | "11111111","123456789012","095669871260","LineItem","25470360326518537103800084","Amazon Elastic Compute Cloud","913846","38802709","205266","DataTransfer-Out-Bytes","RunInstances","","N","$0.120 per GB - up to 10 TB / month data transfer out","2014-09-01 00:00:00","2014-09-01 01:00:00","0.00001009","0.11997536721028","0.00000121","0.1200000000","0.00000121","i-345678901",, 7 | "11111111","123456789012","095669871260","LineItem","25470360326518537103800085","Amazon Elastic Compute Cloud","913846","38802709","205266","DataTransfer-Out-Bytes","RunInstances","","N","$0.120 per GB - up to 10 TB / month data transfer out","2014-09-01 00:00:00","2014-09-01 01:00:00","0.00002698","0.11997536721028","0.00000324","0.1200000000","0.00000324","i-456789012",, 8 | "11111111","123456789012","095669871260","LineItem","25470489453727053070709549","Amazon Simple Storage Service","2226972","38802706","369123","Requests-Tier1","ListBucket","","N","$0.005 per 1,000 PUT, COPY, POST, or LIST requests","2014-09-01 00:00:00","2014-09-01 01:00:00","7.00000000","0.0000050000","0.00003500","0.0000050000","0.00003500","foo",, 9 | "11111111","123456789012","095669871260","LineItem","25470489453727053070709550","Amazon Simple Storage Service","2226972","38802706","369123","Requests-Tier1","ListBucket","","N","$0.005 per 1,000 PUT, COPY, POST, or LIST requests","2014-09-01 00:00:00","2014-09-01 01:00:00","4.00000000","0.0000050000","0.00002000","0.0000050000","0.00002000","bar",, 10 | -------------------------------------------------------------------------------- /tests/unit/test_costs.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2014 Scopely, Inc. 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 | import unittest 14 | import os 15 | import decimal 16 | 17 | import details 18 | 19 | 20 | def path(filename): 21 | return os.path.join(os.path.dirname(__file__), 'data', filename) 22 | 23 | COLUMNS = ['InvoiceID', 24 | 'PayerAccountId', 25 | 'LinkedAccountId', 26 | 'RecordType', 27 | 'RecordId', 28 | 'ProductName', 29 | 'RateId', 30 | 'SubscriptionId', 31 | 'PricingPlanId', 32 | 'UsageType', 33 | 'Operation', 34 | 'AvailabilityZone', 35 | 'ReservedInstance', 36 | 'ItemDescription', 37 | 'UsageStartDate', 38 | 'UsageEndDate', 39 | 'UsageQuantity', 40 | 'BlendedRate', 41 | 'BlendedCost', 42 | 'UnBlendedRate', 43 | 'UnBlendedCost', 44 | 'ResourceId', 45 | 'user:Name', 46 | 'user:Role'] 47 | 48 | SERVICES = ['Amazon Simple Storage Service', 49 | 'Amazon Simple Queue Service', 50 | 'Amazon Elastic Compute Cloud'] 51 | 52 | 53 | class TestCosts(unittest.TestCase): 54 | 55 | def setUp(self): 56 | pass 57 | 58 | def tearDown(self): 59 | pass 60 | 61 | def test_costs(self): 62 | csv_file = path('detailed_with_tags.csv') 63 | total = details.load(csv_file) 64 | cols = total.columns 65 | for col in cols: 66 | self.assertIn(col, COLUMNS) 67 | for col in COLUMNS: 68 | self.assertIn(col, cols) 69 | values = total.values('ProductName') 70 | for value in values: 71 | self.assertIn(value, SERVICES) 72 | for value in SERVICES: 73 | self.assertIn(value, values) 74 | self.assertEqual(total.cost, decimal.Decimal('0.08805964')) 75 | self.assertEqual(total.unblended_cost, decimal.Decimal('0.08805964')) 76 | self.assertEqual(total.blended_cost, decimal.Decimal('0.08805964')) 77 | ec2 = total.filter([('ProductName', '.*Elastic Compute.*')]) 78 | self.assertEqual(ec2.cost, decimal.Decimal('0.08800464')) 79 | s3dt = total.filter([('ProductName', '.*Storage.*'), 80 | ('UsageType', '.*DataTransfer.*')]) 81 | self.assertEqual(s3dt.cost, decimal.Decimal('0.00005964')) 82 | --------------------------------------------------------------------------------