├── .gitignore ├── documentation.pptx ├── README.md └── index.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.tmp 3 | ~*.* 4 | *.xlsx 5 | -------------------------------------------------------------------------------- /documentation.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/longhorn09/aws_prices/HEAD/documentation.pptx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws_prices 2 | Fetching AWS price quotes related to 3Y All Upfront Compute Savings Plan with output in an Excel file. 3 | Also pulls in `1yrNoUpfront` Compute Savings plan as of 12/03/2020. 4 | 5 | ### Installation 6 | ``` 7 | git clone git@github.com:longhorn09/aws_prices.git 8 | pip3 install xlsxwriter 9 | ``` 10 | 11 | ### Running 12 | ``` 13 | python3 index.py 14 | ``` 15 | 16 | ### Output 17 | Python script will create an Excel file with Savings Plan quotes and filename of 18 | ``` 19 | sp_prices.xlsx 20 | ``` 21 | in same folder where python script is run. Can easily filter or use `vlookup()` or any other excel lookup technique such as `xlookup()` or `index/match` 22 | 23 | ![Excel screenshot](https://user-images.githubusercontent.com/11417589/89704400-28320d00-d919-11ea-87a8-5fd1e06f4b66.png) 24 | 25 | 26 | ### Issues 27 | If the Excel file isn't created upon running the script, the likely cause is due to executing Python within WSL (Ubuntu) environment. I think there may be a hidden permissions issue that recently surfaced that oddly doesn't trigger any error messages nor warnings. 28 | 29 | The workaround is to install Python to Windows natively and run python from the DOS prompt command line. At which point the `.xlsx` output file will be created. 30 | 31 | ### Dependencies 32 | Script relies upon `xlsxwriter` 33 | This is for writing to Excel workbook. To install use `pip3` as shown below 34 | ``` 35 | pip3 install xlsxwriter 36 | ``` 37 | ### Performance tweaks 38 | Each regional savings plan URL ranges from 40 MB to 0.1GB. 39 | The list of EC2 offers is roughly 1.3GB. Even over gigabit fiber connections, it can take a while to download/read. 40 | Because of this, if repeatedly running this script, save the 1.3GB JSON file locally, and toggle within the `getSKUListLocal` function and tweak the file name accordingly. 41 | 42 | These toggles are around line 142 and line 224 of the code base. 43 | `doLocal = True` or `doLocal = False` 44 | 45 | ``` 46 | doLocal = True # toggle 47 | 48 | if (doLocal): 49 | # this is a 1.3 GB file - may take time 50 | with open('index_aws_ec2.json') as json_file: 51 | myJSON = json.load(json_file) 52 | ``` 53 | 54 | 55 | ### JSON reference snippets 56 | When reading from [Offer index file](https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json) for `AmazonEC2`, look to `currentVersionUrl` and `currentSavingsPlanIndexUrl` 57 | 58 | ``` 59 | "AmazonEC2" : { 60 | "offerCode" : "AmazonEC2", 61 | "versionIndexUrl" : "/offers/v1.0/aws/AmazonEC2/index.json", 62 | "currentVersionUrl" : "/offers/v1.0/aws/AmazonEC2/current/index.json", 63 | "currentRegionIndexUrl" : "/offers/v1.0/aws/AmazonEC2/current/region_index.json", 64 | "savingsPlanVersionIndexUrl" : "/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/current/index.json", 65 | "currentSavingsPlanIndexUrl" : "/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/current/region_index.json" 66 | }, 67 | ``` 68 | 69 | When looking up respective savings plan Url in [region_index.json](https://pricing.us-east-1.amazonaws.com/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/current/region_index.json), look to `versionUrl` 70 | 71 | ``` 72 | "regions" : [ { 73 | "regionCode" : "ap-south-1", 74 | "versionUrl" : "/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/20200806153551/ap-south-1/index.json" 75 | }, { 76 | ``` 77 | 78 | 3Y Compute Savings Plan All Upfront has product sku of `RQRC4CUNT9HUG9WC` 79 | ``` 80 | { 81 | "sku" : "RQRC4CUNT9HUG9WC", 82 | "productFamily" : "ComputeSavingsPlans", 83 | "serviceCode" : "ComputeSavingsPlans", 84 | "usageType" : "ComputeSP:3yrAllUpfront", 85 | "operation" : "", 86 | "attributes" : { 87 | "purchaseOption" : "All Upfront", 88 | "granularity" : "hourly", 89 | "purchaseTerm" : "3yr", 90 | "locationType" : "AWS Region", 91 | "location" : "Any" 92 | } 93 | } 94 | ``` 95 | 96 | Once the product sku is found, then combine that with the other sku to match by `rateCode` so you can find the correct corresponding `discountedRate` 97 | ``` 98 | , { 99 | "discountedSku" : "TBV6C3VKSXKFHHSC", 100 | "discountedUsageType" : "USE2-BoxUsage:t3a.xlarge", 101 | "discountedOperation" : "RunInstances", 102 | "discountedServiceCode" : "AmazonEC2", 103 | "rateCode" : "RQRC4CUNT9HUG9WC.TBV6C3VKSXKFHHSC", 104 | "unit" : "Hrs", 105 | "discountedRate" : { 106 | "price" : "0.0679", 107 | "currency" : "USD" 108 | } 109 | ``` 110 | 111 | 112 | ### Reference links 113 | 114 | [AWS Bulk API](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/using-ppslong.html) 115 | [Savings plan offer file](https://docs.aws.amazon.com/awsaccountbilling/latest/aboutv2/sp-offer-file.html) 116 | [JSON Editor online](https://jsoneditoronline.org/#left=url.https%3A%2F%2Fpricing.us-east-1.amazonaws.com%2FsavingsPlan%2Fv1.0%2Faws%2FAWSComputeSavingsPlan%2F20200806153551%2Fus-east-2%2Findex.json) 117 | [AmazonEC2.currentVersionUrl (1.3GB)](https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/AmazonEC2/current/index.json) 118 | 119 | 120 | -------------------------------------------------------------------------------- /index.py: -------------------------------------------------------------------------------- 1 | import re # for regular expression, to parse out instance size from instanceType attribute 2 | import json # need this library to interact with JSON data structures 3 | import urllib.request # need this library to open up remote website 4 | import xlsxwriter # pip3 install xlsxwriter , xlwt doesn't support .xlsx 5 | import sys 6 | from operator import itemgetter, attrgetter # https://docs.python.org/3/howto/sorting.html 7 | 8 | class SKUClass: 9 | def __init__(self,pFam,pSize, pRegionCode, pSKU, pOS,pUsageType): 10 | self.instanceFamily = pFam 11 | self.instanceSize = pSize 12 | self.regionCode = pRegionCode 13 | self.sku = pSKU 14 | self.os = pOS 15 | self.rateCode = '' 16 | self.price = 0.0 17 | self.usageType = pUsageType 18 | self.price1yrNoUpfront = 0.0 19 | self.rateCode2 = '' 20 | 21 | ######################################################################################### 22 | # offer index file: https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json 23 | ######################################################################################### 24 | class AWSPricing: 25 | ROOT_URL = 'https://pricing.us-east-1.amazonaws.com' 26 | 27 | region_map={ 28 | # Americas 29 | "CMH": ("us-east-2", "US East (Ohio)"), 30 | "IAD": ("us-east-1","US East (N. Virginia)"), 31 | "PDX": ("us-west-2","US West (Oregon)"), 32 | "SFO": ("us-west-1","US West (N. California)"), 33 | 34 | ##### us-west-2-lax-1a, us-west-2-lax-1b 35 | # doesn't seem to work 36 | #"LAX": ("us-west-2-lax-1a","US West (Los Angeles)"), # https://aws.amazon.com/blogs/aws/announcing-a-second-local-zone-in-los-angeles/ 37 | 38 | # Canada 39 | "YYZ": ("ca-central-1","Canada (Central)"), # Toronto Pearson International 40 | 41 | # LATAM 42 | "GRU": ("sa-east-1","South America (Sao Paulo)"), 43 | 44 | # ME / Africa 45 | "BAH": ("me-south-1","Middle East (Bahrain)"), 46 | "CPT": ("af-south-1","Africa (Cape Town)"), 47 | # APAC 48 | "HKG": ("ap-east-1","Asia Pacific (Hong Kong)"), 49 | "BOM": ("ap-south-1","Asia Pacific (Mumbai)"), 50 | "ITM": ("ap-northeast-3","Asia Pacific (Osaka-Local)"), 51 | "ICN": ("ap-northeast-2","Asia Pacific (Seoul)"), 52 | "SIN": ("ap-southeast-1","Asia Pacific (Singapore)"), 53 | "SYD": ("ap-southeast-2","Asia Pacific (Sydney)"), 54 | "NRT": ("ap-northeast-1","Asia Pacific (Tokyo)"), 55 | # EU 56 | "FRA": ("eu-central-1","EU (Frankfurt)"), 57 | "DUB": ("eu-west-1", "EU (Ireland)"), 58 | "LHR": ("eu-west-2","EU (London)"), 59 | "MXP": ("eu-south-1","EU (Milan)"), 60 | "CDG": ("eu-west-3","EU (Paris)"), 61 | "ARN": ("eu-north-1","EU (Stockholm)") 62 | 63 | 64 | } 65 | 66 | def __init__(self): 67 | super().__init__() 68 | 69 | def getSavingsPlanURL(self): 70 | return "ok" 71 | 72 | ####################################################################### 73 | # first check the offer index file to get the paths to the savings plan index Url 74 | ####################################################################### 75 | def getOfferIndexURL(self): 76 | retvalue = None 77 | url = None 78 | contents = None 79 | myJSON = None 80 | ## end of variable declaration 81 | 82 | url = 'https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json' # simply lookup "AmazonEC2" , then "currentSavingsPlanIndexUrl" 83 | 84 | contents = urllib.request.urlopen(url).read() 85 | myJSON = json.loads(contents) 86 | retvalue = (myJSON["offers"]["AmazonEC2"]["currentSavingsPlanIndexUrl"]).strip() #ie. https://pricing.us-east-1.amazonaws.com/savingsPlan/v1.0/aws/AWSComputeSavingsPlan/current/region_index.json 87 | 88 | return retvalue 89 | 90 | # ie. for IAD this returns "us-east-1" 91 | def getAWSRegionFromCode(self, pRegionCode): 92 | return self.region_map.get(pRegionCode,(None,None))[0] 93 | 94 | # ie. for IAD this returns "US East (N. Virginia)" 95 | def getAWSLocationFromCode(self,pRegionCode): 96 | return self.region_map.get(pRegionCode,(None,None))[1] 97 | 98 | ####################################################################### 99 | # URL lookup for region SP version Url 100 | # @pArg1 - the 3 letter region to lookup (ie. the airport code) 101 | # @pArg2 - URL to fetch savings plan JSON 102 | ####################################################################### 103 | def getSavingsPlanPriceListUrlForRegion(self, pArg1, pArg2): 104 | url = None 105 | contents = None 106 | myJSON = None 107 | retvalue = None 108 | versionUrlPath = None 109 | 110 | regionId = self.getAWSRegionFromCode(pArg1) # convert 3 letter airport code IAD to 'us-east-1' 111 | url = self.ROOT_URL + pArg2 112 | contents = urllib.request.urlopen(url).read() 113 | myJSON = json.loads(contents) 114 | 115 | for x in range(len(myJSON["regions"])): 116 | if ((myJSON["regions"][x]["regionCode"]).strip() == regionId): 117 | versionUrlPath = myJSON["regions"][x]["versionUrl"] 118 | break # get outta the for loop 119 | 120 | 121 | url = self.ROOT_URL + versionUrlPath 122 | retvalue = url 123 | 124 | return retvalue 125 | 126 | ####################################################################### 127 | # URL lookup for region SP version Url 128 | ####################################################################### 129 | def getSKUListLocal(self, pRegionCodeCSV): 130 | myJSON = None 131 | counter = None 132 | instanceType = None 133 | my_list = [] 134 | url = None 135 | doLocal = None 136 | 137 | ############################################ 138 | # [FASTER, stale ] Toggle doLocal to True if JSON already saved locally as index_aws_ec2.json, can use doSaveJSONLocal() for initial save 139 | # [SLOWER, fresher] Toggle doLocal to False to pull from AWS site - this is a 1GB+ sized read 140 | ############################################ 141 | #doLocal = False # True for Dev , false for Prod 142 | doLocal = True # already have a 1.6GB+ JSON saved locally as index_aws_ec2.json 143 | 144 | if (doLocal): 145 | # this is a 1.3 GB file - may take time 146 | with open('index_aws_ec2.json') as json_file: 147 | myJSON = json.load(json_file) # note: json.load() for local file instead of json.loads() 148 | elif (doLocal == False): 149 | url = 'https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json' 150 | contents = urllib.request.urlopen(url).read() 151 | myJSON = json.loads(contents) 152 | 153 | url = self.ROOT_URL + myJSON["offers"]["AmazonEC2"]["currentVersionUrl"] 154 | contents = urllib.request.urlopen(url).read() 155 | myJSON = json.loads(contents) 156 | 157 | regionArr = pRegionCodeCSV.split(",") 158 | 159 | for x in range(len(regionArr)): 160 | #print(regionArr[x] + ': ' + self.getAWSLocationFromCode(regionArr[x])) 161 | for key,value in myJSON["products"].items(): 162 | # regex pattern for ["attributes"]["usagetype"] can be: 163 | # EU-EC2SP:r4.1yrAllUpfront 164 | # EU-BoxUsage:m5.8xlarge 165 | # EUW2-BoxUsage:m5d.xlarge 166 | # BoxUsage:m5d.xlarge 167 | pattern = "^([A-Z0-9\-]+)?BoxUsage:.+$" # make sure BoxUsage, not UnusedBox etc 168 | try: 169 | if (value["productFamily"] == "Compute Instance" and value["attributes"]["servicecode"] == "AmazonEC2" 170 | and (value["attributes"]["operatingSystem"] == "Linux" or value["attributes"]["operatingSystem"] == "RHEL" or value["attributes"]["operatingSystem"] == "Windows") 171 | and value["attributes"]["preInstalledSw"] == "NA" 172 | #and value["attributes"]["instanceFamily"] == "General purpose" 173 | and value["attributes"]["locationType"] == "AWS Region" 174 | and value["attributes"]["tenancy"] == "Shared" 175 | and value["attributes"]["location"] == self.getAWSLocationFromCode(regionArr[x]) 176 | and re.match(pattern,value["attributes"]["usagetype"])): 177 | #print("yCount: " + self.getAWSLocationFromCode(regionArr[x]) + ", sku: " + value["sku"] + ", usageType:" + value["attributes"]["usagetype"]) 178 | pattern = "^(.+)\.([0-9A-Za-z]+)$" 179 | if ("instanceType" in value["attributes"] and re.match(pattern,value["attributes"]["instanceType"])): 180 | m = re.search(pattern, value["attributes"]["instanceType"]) 181 | #if (m.group(2) == "small"): #not all instanceFamily have size small 182 | #print (key + ": " + m.group(0)) 183 | #print(m.group(1) + " " + regionArr[x] + " " + key + " " + value["attributes"]["operatingSystem"]) 184 | my_list.append( SKUClass(m.group(1) 185 | , m.group(2) 186 | , regionArr[x] #pRegionCode 187 | , key 188 | , value["attributes"]["operatingSystem"] 189 | , value["attributes"]["usagetype"]) 190 | ) 191 | except: 192 | print(key + ': no productFamily') 193 | my_list = sorted(my_list, key=attrgetter('regionCode','instanceFamily','instanceSize')) 194 | 195 | return my_list 196 | 197 | ############################################################################ 198 | # Description: can handle multiple regions based on provided CSV list 199 | # @pArg1 - this is a CSV list of regions by 3 letter airport code 200 | # @pArg2 - this is the list Array of SKUClass objects 201 | ############################################################################ 202 | # JSON structure - https://jsoneditoronline.org/ 203 | # -products 204 | # -terms 205 | # └savingsPlan 206 | # └ sku 207 | # └ rates 208 | # └ rateCode "RQRC4CUNT9HUG9WC.TBV6C3VKSXKFHHSC" 209 | # └ discountedRate 210 | # └ price "0.0679" 211 | ############################################################################ 212 | def getSavingsPlanPrices2(self,pArg1, pArg2): 213 | contents = None 214 | myJSON = None 215 | doLocal = None 216 | spURL = None 217 | regionURL = None 218 | productSku = None 219 | productSku1yrNoUpfront = None 220 | 221 | # END VARIABLE DECLARATION 222 | 223 | spURL = self.getOfferIndexURL() # gets the current savings plan URL, which is an index of all the regions' savings plan URLs 224 | doLocal = False # set to false for production 225 | 226 | for regionSplitLoop in range(len(pArg1.strip().split(","))): 227 | productSku = None 228 | productSku1yrNoUpfront = None 229 | regionURL = self.getSavingsPlanPriceListUrlForRegion(pArg1.strip().split(",")[regionSplitLoop], spURL) 230 | 231 | if (doLocal == False): 232 | print('[' + pArg1.strip().split(",")[regionSplitLoop] + '] ' + regionURL) 233 | contents = urllib.request.urlopen(regionURL).read() 234 | myJSON = json.loads(contents) # for production - use actual web url (slower) 235 | elif (doLocal == True): 236 | with open('CMH.json') as json_file: 237 | myJSON = json.load(json_file) 238 | 239 | # this loop to get the sku that corresponds with 3yr All Upfront ComputeSavingsPlan 240 | # later also look for 1yr No upfront Compute Savings plan 241 | for item in myJSON["products"]: 242 | if (item["usageType"] == "ComputeSP:3yrAllUpfront" and item["productFamily"] == "ComputeSavingsPlans"): 243 | productSku = item["sku"] 244 | elif (item["usageType"] == "ComputeSP:1yrNoUpfront" and item["productFamily"] == "ComputeSavingsPlans"): 245 | productSku1yrNoUpfront = item["sku"] 246 | if (productSku is not None and productSku1yrNoUpfront is not None ): 247 | #print ('productSku1yrNoUpfront: ' + productSku1yrNoUpfront) 248 | break 249 | 250 | # now get the actual rates in the "terms" section of the JSON 251 | for item in myJSON["terms"]["savingsPlan"]: 252 | if (item["sku"] == productSku): 253 | foundRateList = item["rates"] 254 | break 255 | 256 | # find the price by rateCode - ie. "RQRC4CUNT9HUG9WC.TBV6C3VKSXKFHHSC" 257 | for x in range(len(pArg2)): 258 | #print("getSavingsPlanPrices2: [" + pArg2[x].regionCode + "]: " + pArg2[x].sku + ", os: " + pArg2[x].os + ", " + pArg2[x].instanceFamily + "." + pArg2[x].instanceSize) 259 | for item in foundRateList: 260 | if (item['rateCode'] == productSku + '.' + pArg2[x].sku): 261 | pArg2[x].price = item['discountedRate']['price'] 262 | pArg2[x].rateCode = item['rateCode'] 263 | #print(item['rateCode'] + ": " + pArg2[x].price + ", " + pArg2[x].instanceFamily + ", " + pArg2[x].instanceSize+ ", " + pArg2[x].os) 264 | break 265 | 266 | ### repeat same loops but for 1yrNoUpfront savings plan 267 | for item in myJSON["terms"]["savingsPlan"]: 268 | if (item["sku"] == productSku1yrNoUpfront): 269 | foundRateList = item["rates"] 270 | break 271 | 272 | # find the price by rateCode - ie. "RQRC4CUNT9HUG9WC.TBV6C3VKSXKFHHSC" 273 | for x in range(len(pArg2)): 274 | for item in foundRateList: 275 | if (item['rateCode'] == productSku1yrNoUpfront + '.' + pArg2[x].sku): 276 | pArg2[x].price1yrNoUpfront = item['discountedRate']['price'] 277 | pArg2[x].rateCode2 = item['rateCode'] 278 | break 279 | 280 | return pArg2 281 | 282 | ############################################################## 283 | # @pArg1 the list of SKUClass objects 284 | ############################################################## 285 | def doWriteExcel(self,pArg1): 286 | counter = 2 287 | blankCount = 0 288 | 289 | try: 290 | book = xlsxwriter.Workbook('sp_prices.xlsx') 291 | sheet1 = book.add_worksheet('prices') 292 | 293 | money = book.add_format({'num_format': '#,##0.0000'}) # https://xlsxwriter.readthedocs.io/tutorial02.html 294 | ##################################### 295 | # write headers in row 1 296 | ##################################### 297 | sheet1.write_string('A1','RegionCode') 298 | sheet1.write_string('B1','Region') 299 | sheet1.write_string('C1','Location') 300 | sheet1.write_string('D1','OS') 301 | sheet1.write_string('E1','InstanceFamily') 302 | sheet1.write_string('F1','Size') 303 | sheet1.write_string('G1','rateCode') 304 | sheet1.write_string('H1','usageType') 305 | sheet1.write_string('I1','3yrAllUpfront') 306 | sheet1.write_string('J1','1yrNoUpfront') 307 | sheet1.write_string('K1','rateCode1yrNoUpfront') 308 | 309 | sheet1.set_column('B:C',14) 310 | sheet1.set_column('G:G',43) 311 | #print("len(pArg1): " + len(pArg1)) 312 | for x in range(len(pArg1)): 313 | if (float(pArg1[x].price) > 0): 314 | sheet1.write_string('A' + str(counter), pArg1[x].regionCode) 315 | sheet1.write_string('B' + str(counter), self.getAWSRegionFromCode(pArg1[x].regionCode)) 316 | sheet1.write_string('C' + str(counter), self.getAWSLocationFromCode(pArg1[x].regionCode)) 317 | sheet1.write_string('D' + str(counter), pArg1[x].os) 318 | sheet1.write_string('E' + str(counter), pArg1[x].instanceFamily) 319 | sheet1.write_string('F' + str(counter), pArg1[x].instanceSize) 320 | sheet1.write_string('G' + str(counter), pArg1[x].rateCode) 321 | sheet1.write_string('H' + str(counter), pArg1[x].usageType) 322 | sheet1.write_number('I' + str(counter), float(pArg1[x].price),money) 323 | sheet1.write_number('J' + str(counter), float(pArg1[x].price1yrNoUpfront),money) #float(pArg1[x].price1yrNoUpfront),money) 324 | sheet1.write_string('K' + str(counter), pArg1[x].rateCode2) 325 | #print(pArg1[x].regionCode + ', ' + pArg1[x].os + ', ' + pArg1[x].instanceFamily + ', ' + pArg1[x].instanceSize + ', 3yr: ' + pArg1[x].price + ', 1yr: ' + pArg1[x].price1yrNoUpfront) 326 | counter += 1 # this increments the Excel output row 327 | else: 328 | blankCount += 1 329 | print('blankCount (https://github.com/longhorn09/aws_prices/issues/1): ' + str(blankCount)) 330 | book.close() # close the excel file 331 | except: 332 | print("doWriteExcel(): Error trying to write to Excel",sys.exc_info()[0],"occurred.") 333 | ####################################################### 334 | # Run this once to create a local copy of large 1.3GB JSON file for local development and testing purposes 335 | ####################################################### 336 | def doSaveJSONLocal(self): 337 | url = 'https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws/index.json' 338 | 339 | try: 340 | contents = urllib.request.urlopen(url).read() 341 | myJSON = json.loads(contents) 342 | 343 | url = self.ROOT_URL + myJSON["offers"]["AmazonEC2"]["currentVersionUrl"] 344 | #print(url) 345 | contents = urllib.request.urlopen(url).read() 346 | myJSON = json.loads(contents) 347 | except: 348 | print("doSaveJSONLocal(): Error reading JSON From AWS",sys.exc_info()[0],"occurred.") 349 | 350 | try: 351 | with open('index_aws_ec2.json','w') as outfile: 352 | json.dump(myJSON, outfile) 353 | outfile.close() 354 | except: 355 | print("doSaveJSONLocal(): Error trying to write Excel file",sys.exc_info()[0],"occurred.") 356 | 357 | ############################################ 358 | # MAIN CODE EXECUTION BEGIN 359 | ############################################ 360 | if __name__ == '__main__': 361 | listArr = [] 362 | regionURL = None 363 | 364 | # regionsArg expects a CSV list of 3 letter airport region codes 365 | # tweak as necessary for the regions of interest 366 | # issues with ITM and BOM? 367 | #regionsArg = "CMH,LHR,FRA,IAD,PDX,SIN,GRU,NRT,DUB,SYD,CDG,ICN,SFO" 368 | regionsArg = "" 369 | regionsArg = regionsArg + "CMH" # US East (Ohio) 370 | regionsArg = regionsArg + ",LHR" # EU (London) 371 | regionsArg = regionsArg + ",FRA" # EU (Frankfurt) 372 | regionsArg = regionsArg + ",IAD" # US East (N. Virginia) 373 | regionsArg = regionsArg + ",PDX" # US West (Oregon) 374 | regionsArg = regionsArg + ",SIN" # Asia Pacific (Singapore) 375 | regionsArg = regionsArg + ",GRU" # South America (Sao Paulo) 376 | regionsArg = regionsArg + ",NRT" # Asia Pacific (Tokyo) 377 | regionsArg = regionsArg + ",DUB" # EU (Ireland) 378 | regionsArg = regionsArg + ",SYD" # Asia Pacific (Sydney) 379 | regionsArg = regionsArg + ",CDG" # EU (Paris) 380 | regionsArg = regionsArg + ",ICN" # Asia Pacific (Seoul) 381 | regionsArg = regionsArg + ",SFO" # US West (N. California) 382 | regionsArg = regionsArg + ",CPT" # Africa (Cape Town) 383 | regionsArg = regionsArg + ",MXP" # EU (Milan) 384 | regionsArg = regionsArg + ",BAH" # Middle East (Bahrain) 385 | regionsArg = regionsArg + ",ARN" # EU (Stockholm) 386 | regionsArg = regionsArg + ",HKG" # Asia Pacific (Hong Kong) 387 | regionsArg = regionsArg + ",YYZ" # Canada (Central) 388 | #issues with LAX & ITM , ie. Local regions 389 | 390 | myObj = AWSPricing() # object instantiation 391 | 392 | # do this once to save a 1GB+ JSON locally for local development, and comment all lines of code after myObj.doSaveJSONLocal() 393 | # for faster performance, just copy/paste the appropriate URL into your browser and save off/rename the JSON retrieved to index_aws_ec2.json 394 | # myObj.doSaveJSONLocal() 395 | 396 | listArr = myObj.getSKUListLocal(regionsArg) # loops thru the big 1GB+ JSON, to get the appropriate product SKUs for a region 397 | 398 | listArr = myObj.getSavingsPlanPrices2(regionsArg, listArr) 399 | myObj.doWriteExcel(listArr) 400 | 401 | 402 | --------------------------------------------------------------------------------