| #!/opt/homebrew/bin/python3 |
| # |
| # units_cur for units, a program for updated currency exchange rates |
| # |
| # Copyright (C) 2017-2018, 2022 |
| # Free Software Foundation, Inc |
| # |
| # This program is free software; you can redistribute it and/or modify |
| # it under the terms of the GNU General Public License as published by |
| # the Free Software Foundation; either version 3 of the License, or |
| # (at your option) any later version. |
| # |
| # This program is distributed in the hope that it will be useful, |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| # GNU General Public License for more details. |
| # |
| # You should have received a copy of the GNU General Public License |
| # along with this program; if not, write to the Free Software |
| # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
| # |
| # |
| # This program was written by Adrian Mariano (adrianm@gnu.org) |
| # |
| |
| # For Python 2 & 3 compatibility |
| from __future__ import absolute_import, division, print_function |
| # |
| # |
| |
| version = '5.0' |
| |
| # Version 5.0: |
| # |
| # Rewrite to support multiple different data sources due to disappearance |
| # of the Yahoo feed. Includes support for base currency selection. |
| # |
| # Version 4.3: 20 July 2018 |
| # |
| # Validate rate data from server |
| # |
| # Version 4.2: 18 April 2018 |
| # |
| # Handle case of empty/malformed entry returned from the server |
| # |
| # Version 4.1: 30 October 2017 |
| # |
| # Fixed to include USD in the list of currency codes. |
| # |
| # Version 4: 2 October 2017 |
| # |
| # Complete rewrite to use Yahoo YQL API due to removal of TimeGenie RSS feed. |
| # Switched to requests library using JSON. One program now runs under |
| # Python 2 or Python 3. Thanks to Ray Hamel for some help with this update. |
| |
| # Normal imports |
| import requests |
| import codecs |
| import json |
| from argparse import ArgumentParser |
| from collections import OrderedDict |
| from datetime import date |
| from os import linesep |
| from os.path import getmtime |
| from sys import exit, stderr, stdout |
| |
| datestr = date.today().isoformat() |
| |
| output_dir='/usr/local/com/units/' |
| |
| currency_file = output_dir + 'currency.units' |
| cpi_file = output_dir + 'cpi.units' |
| |
| # valid metals |
| |
| validmetals = ['silver','gold','platinum'] |
| |
| PRIMITIVE = '! # Base unit, the primitive unit of currency' |
| |
| # This exchange rate table lists the currency ISO 4217 codes, their |
| # long text names, and any fixed definitions. If the definition is |
| # empty then units_cur will query the server for a value. |
| |
| rate_index = 1 |
| currency = OrderedDict([ |
| ('ATS', ['austriaschilling', '1|13.7603 euro']), |
| ('BEF', ['belgiumfranc', '1|40.3399 euro']), |
| ('CYP', ['cypruspound', '1|0.585274 euro']), |
| ('EEK', ['estoniakroon', '1|15.6466 euro # Equal to 1|8 germanymark']), |
| ('FIM', ['finlandmarkka', '1|5.94573 euro']), |
| ('FRF', ['francefranc', '1|6.55957 euro']), |
| ('DEM', ['germanymark', '1|1.95583 euro']), |
| ('GRD', ['greecedrachma', '1|340.75 euro']), |
| ('IEP', ['irelandpunt', '1|0.787564 euro']), |
| ('ITL', ['italylira', '1|1936.27 euro']), |
| ('LVL', ['latvialats', '1|0.702804 euro']), |
| ('LTL', ['lithuanialitas', '1|3.4528 euro']), |
| ('LUF', ['luxembourgfranc', '1|40.3399 euro']), |
| ('MTL', ['maltalira', '1|0.4293 euro']), |
| ('SKK', ['slovakiakoruna', '1|30.1260 euro']), |
| ('SIT', ['sloveniatolar', '1|239.640 euro']), |
| ('ESP', ['spainpeseta', '1|166.386 euro']), |
| ('NLG', ['netherlandsguilder','1|2.20371 euro']), |
| ('PTE', ['portugalescudo', '1|200.482 euro']), |
| ('CVE', ['capeverdeescudo', '1|110.265 euro']), |
| ('BGN', ['bulgarialev', '1|1.9558 euro']), |
| ('BAM', ['bosniaconvertiblemark','germanymark']), |
| ('KMF', ['comorosfranc', '1|491.96775 euro']), |
| ('XOF', ['westafricafranc', '1|655.957 euro']), |
| ('XPF', ['cfpfranc', '1|119.33 euro']), |
| ('XAF', ['centralafricacfafranc','1|655.957 euro']), |
| ('AED', ['uaedirham','']), |
| ('AFN', ['afghanistanafghani','']), |
| ('ALL', ['albanialek','']), |
| ('AMD', ['armeniadram','']), |
| ('ANG', ['antillesguilder','']), |
| ('AOA', ['angolakwanza','']), |
| ('ARS', ['argentinapeso','']), |
| ('AUD', ['australiadollar','']), |
| ('AWG', ['arubaflorin','']), |
| ('AZN', ['azerbaijanmanat','']), |
| ('BAM', ['bosniaconvertiblemark','']), |
| ('BBD', ['barbadosdollar','']), |
| ('BDT', ['bangladeshtaka','']), |
| ('BGN', ['bulgarialev','']), |
| ('BHD', ['bahraindinar','']), |
| ('BIF', ['burundifranc','']), |
| ('BMD', ['bermudadollar','']), |
| ('BND', ['bruneidollar','']), |
| ('BOB', ['boliviaboliviano','']), |
| ('BRL', ['brazilreal','']), |
| ('BSD', ['bahamasdollar','']), |
| ('BTN', ['bhutanngultrum','']), |
| ('BWP', ['botswanapula','']), |
| ('BYN', ['belarusruble','']), |
| ('BYR', ['oldbelarusruble','1|10000 BYN']), |
| ('BZD', ['belizedollar','']), |
| ('CAD', ['canadadollar','']), |
| ('CDF', ['drcfranccongolais','']), |
| ('CHF', ['swissfranc','']), |
| ('CLP', ['chilepeso','']), |
| ('CNY', ['chinayuan','']), |
| ('COP', ['colombiapeso','']), |
| ('CRC', ['costaricacolon','']), |
| ('CUP', ['cubapeso','']), |
| ('CVE', ['capeverdeescudo','']), |
| ('CZK', ['czechiakoruna','']), |
| ('DJF', ['djiboutifranc','']), |
| ('DKK', ['denmarkkrone','']), |
| ('DOP', ['dominicanrepublicpeso','']), |
| ('DZD', ['algeriadinar','']), |
| ('EGP', ['egyptpound','']), |
| ('ERN', ['eritreanakfa','']), |
| ('ETB', ['ethiopiabirr','']), |
| ('EUR', ['euro','']), |
| ('FJD', ['fijidollar','']), |
| ('FKP', ['falklandislandspound','']), |
| ('GBP', ['ukpound','']), |
| ('GEL', ['georgialari','']), |
| ('GHS', ['ghanacedi','']), |
| ('GIP', ['gibraltarpound','']), |
| ('GMD', ['gambiadalasi','']), |
| ('GNF', ['guineafranc','']), |
| ('GTQ', ['guatemalaquetzal','']), |
| ('GYD', ['guyanadollar','']), |
| ('HKD', ['hongkongdollar','']), |
| ('HNL', ['honduraslempira','']), |
| ('HRK', ['croatiakuna','']), |
| ('HTG', ['haitigourde','']), |
| ('HUF', ['hungaryforint','']), |
| ('IDR', ['indonesiarupiah','']), |
| ('ILS', ['israelnewshekel','']), |
| ('INR', ['indiarupee','']), |
| ('IQD', ['iraqdinar','']), |
| ('IRR', ['iranrial','']), |
| ('ISK', ['icelandkrona','']), |
| ('JMD', ['jamaicadollar','']), |
| ('JOD', ['jordandinar','']), |
| ('JPY', ['japanyen','']), |
| ('KES', ['kenyaschilling','']), |
| ('KGS', ['kyrgyzstansom','']), |
| ('KHR', ['cambodiariel','']), |
| ('KMF', ['comorosfranc','']), |
| ('KPW', ['northkoreawon','']), |
| ('KRW', ['southkoreawon','']), |
| ('KWD', ['kuwaitdinar','']), |
| ('KYD', ['caymanislandsdollar','']), |
| ('KZT', ['kazakhstantenge','']), |
| ('LAK', ['laokip','']), |
| ('LBP', ['lebanonpound','']), |
| ('LKR', ['srilankarupee','']), |
| ('LRD', ['liberiadollar','']), |
| ('LSL', ['lesotholoti','']), |
| ('LYD', ['libyadinar','']), |
| ('MAD', ['moroccodirham','']), |
| ('MDL', ['moldovaleu','']), |
| ('MGA', ['madagascarariary','']), |
| ('MKD', ['macedoniadenar','']), |
| ('MMK', ['myanmarkyat','']), |
| ('MNT', ['mongoliatugrik','']), |
| ('MOP', ['macaupataca','']), |
| ('MRO', ['mauritaniaoldouguiya','1|10 MRU']), |
| ('MRU', ['mauritaniaouguiya', '']), |
| ('MUR', ['mauritiusrupee','']), |
| ('MVR', ['maldiverufiyaa','']), |
| ('MWK', ['malawikwacha','']), |
| ('MXN', ['mexicopeso','']), |
| ('MYR', ['malaysiaringgit','']), |
| ('MZN', ['mozambiquemetical','']), |
| ('NAD', ['namibiadollar','']), |
| ('NGN', ['nigerianaira','']), |
| ('NIO', ['nicaraguacordobaoro','']), |
| ('NOK', ['norwaykrone','']), |
| ('NPR', ['nepalrupee','']), |
| ('NZD', ['newzealanddollar','']), |
| ('OMR', ['omanrial','']), |
| ('PAB', ['panamabalboa','']), |
| ('PEN', ['perunuevosol','']), |
| ('PGK', ['papuanewguineakina','']), |
| ('PHP', ['philippinepeso','']), |
| ('PKR', ['pakistanrupee','']), |
| ('PLN', ['polandzloty','']), |
| ('PYG', ['paraguayguarani','']), |
| ('QAR', ['qatarrial','']), |
| ('RON', ['romanianewlei','']), |
| ('RSD', ['serbiadinar','']), |
| ('RUB', ['russiaruble','']), |
| ('RWF', ['rwandafranc','']), |
| ('SAR', ['saudiarabiariyal','']), |
| ('SBD', ['solomonislandsdollar','']), |
| ('SCR', ['seychellesrupee','']), |
| ('SDG', ['sudanpound','']), |
| ('SEK', ['swedenkrona','']), |
| ('SGD', ['singaporedollar','']), |
| ('SHP', ['sainthelenapound','']), |
| # ('SLL', ['sierraleoneoldleone','1|1000 SLE']), |
| # ('SLE', ['sierraleoneleone','']), |
| ('SOS', ['somaliaschilling','']), |
| ('SRD', ['surinamedollar','']), |
| ('SSP', ['southsudanpound','']), |
| ('STD', ['saotome&principeolddobra','']), |
| ('STN', ['saotome&principedobra','']), |
| # ('SVC', ['elsalvadorcolon','']), # eliminated in 2001 |
| ('SYP', ['syriapound','']), |
| ('SZL', ['swazilandlilangeni','']), |
| ('THB', ['thailandbaht','']), |
| ('TJS', ['tajikistansomoni','']), |
| ('TMT', ['turkmenistanmanat','']), |
| ('TND', ['tunisiadinar','']), |
| ('TOP', ["tongapa'anga",'']), |
| ('TRY', ['turkeylira','']), |
| ('TTD', ['trinidadandtobagodollar','']), |
| ('TWD', ['taiwandollar','']), |
| ('TZS', ['tanzaniashilling','']), |
| ('UAH', ['ukrainehryvnia','']), |
| ('UGX', ['ugandaschilling','']), |
| ('USD', ['US$', '']), |
| ('UYU', ['uruguaypeso','']), |
| ('UZS', ['uzbekistansum','']), |
| ('VEF', ['venezuelabolivarfuerte','']), |
| ('VES', ['venezuelabolivarsoberano','']), |
| ('VND', ['vietnamdong','']), |
| ('VUV', ['vanuatuvatu','']), |
| ('WST', ['samoatala','']), |
| ('XAF', ['centralafricacfafranc','']), |
| ('XCD', ['eastcaribbeandollar','']), |
| ('XDR', ['specialdrawingrights','']), |
| ('YER', ['yemenrial','']), |
| ('ZAR', ['southafricarand','']), |
| ('ZMW', ['zambiakwacha','']), |
| ('ZWL', ['zimbabwedollar','']), |
| ('FOK', ['faroeislandskróna','DKK']), |
| ('GGP', ['guernseypound', 'GBP']), |
| ('IMP', ['isleofmanpound','GBP']), |
| ('JEP', ['jerseypound','GBP']), |
| ('KID', ['kiribatidollar','AUD']), |
| ('TVD', ['tuvaludollar','AUD']), |
| ]) |
| |
| |
| def validfloat(x): |
| try: |
| float(x) |
| return True |
| except ValueError: |
| return False |
| |
| def addrate(verbose,form,code,rate): |
| if code not in currency.keys(): |
| if (verbose): |
| stderr.write('Got unknown currency with code {}\n'.format(code)) |
| else: |
| if not currency[code][rate_index]: |
| if validfloat(rate): |
| currency[code][rate_index] = form.format(rate) |
| else: |
| stderr.write('Got invalid rate "{}" for currency "{}"\n'.format( |
| rate, code)) |
| elif verbose: |
| if currency[code][rate_index] != form.format(rate): |
| stderr.write('Got value "{}" for currency "{}" but ' |
| 'it is already defined as {}\n'.format(rate, code, |
| currency[code][rate_index])) |
| |
| def getjson(address,args=None): |
| try: |
| res = requests.get(address,args) |
| res.raise_for_status() |
| return(res.json()) |
| except requests.exceptions.RequestException as e: |
| stderr.write('Error connecting to currency server:\n{}.\n'.format(e)) |
| exit(1) |
| |
| ######################################################## |
| # |
| # Connect to floatrates for currency update |
| # |
| |
| def floatrates(verbose,base,dummy): |
| webdata = getjson('https://www.floatrates.com/daily/'+base+'.json') |
| for index in webdata: |
| entry = webdata[index] |
| if 'rate' not in entry or 'code' not in entry: # Skip empty/bad entries |
| if verbose: |
| stderr.write('Got bad entry from server: '+str(entry)+'\n') |
| else: |
| addrate(verbose,'{} '+base,entry['code'],entry['inverseRate']) |
| currency[base][rate_index] = PRIMITIVE |
| return('FloatRates ('+base+' base)') |
| |
| ######################################################## |
| # |
| # Connect to European central bank site |
| # |
| |
| def eubankrates(verbose,base,dummy): |
| if verbose and base!='EUR': |
| stderr.write('European bank uses euro for base currency. Specified base {} ignored.\n'.format(base)) |
| import xml.etree.ElementTree as ET |
| try: |
| res=requests.get('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml') |
| res.raise_for_status() |
| data = ET.fromstring(res.content)[2][0] |
| except requests.exceptions.RequestException as e: |
| stderr.write('Error connecting to currency server:\n{}.\n'. |
| format(e)) |
| exit(1) |
| for entry in data.iter(): |
| if entry.get('time'): |
| continue |
| rate = entry.get('rate') |
| code = entry.get('currency') |
| if not rate or not code: # Skip empty/bad entries |
| if verbose: |
| stderr.write('Got bad entry from server, code {} and rate {}\n'.format(code,rate)) |
| else: |
| addrate(verbose,'1|{} euro', code, rate) |
| currency['EUR'][rate_index]=PRIMITIVE |
| return('the European Central Bank (euro base)') |
| |
| ######################################################## |
| # |
| # Connect to fixer.io (requires API key) |
| # |
| # Free API key does not allow changing base currency |
| # With free key only euro base is supported, and https is not allowed |
| # |
| |
| def fixer(verbose,base,key): |
| if not key: |
| stderr.write('API key required for this source\n') |
| exit(1) |
| if verbose and base!='EUR': |
| stderr.write('Fixer uses euro for base currency. Specified base {} ignored.\n'.format(base)) |
| webdata = getjson('http://data.fixer.io/api/latest', {'access_key':key}) |
| if not webdata['success']: |
| stderr.write('Currency server error: '+webdata['error']['info']) |
| exit(1) |
| for code in webdata['rates']: |
| addrate(verbose,'1|{} euro', code, webdata['rates'][code]) |
| currency['EUR'][rate_index] = PRIMITIVE |
| return('Fixer (euro base)') |
| |
| ######################################################## |
| # |
| # Connect to openexchangerates (requires API key) |
| # |
| # Free API key does not allow changing the base currency |
| # |
| |
| def openexchangerates(verbose,base,key): |
| if not key: |
| stderr.write('API key required for this source\n') |
| exit(1) |
| if verbose and base!='USD': |
| stderr.write('Open Exchange Rates uses US dollar for base currency. Specified base {} ignored.\n'.format(base)) |
| webdata = getjson('https://openexchangerates.org/api/latest.json', |
| {'app_id':key} |
| ) |
| for code in webdata['rates']: |
| addrate(verbose,'1|{} US$', code, webdata['rates'][code]) |
| currency['USD'][rate_index] = PRIMITIVE |
| return('open exchange rates (USD base)') |
| |
| ######################################################## |
| # |
| # Connect to exchangerate-api.com |
| # |
| # The open access endpoint gives updates once per day. |
| # User can optionally supply a paid account's API key to get newer data and more precision. |
| # Base currency can be changed with both open and paid versions of the API. |
| # |
| |
| def exchangerate_api(verbose,base,key): |
| if not key: |
| webdata = getjson('https://open.er-api.com/v6/latest/'+base) |
| if not webdata['result'] or webdata['result'] != 'success': |
| stderr.write('Currency server error: '+webdata['error-type']+'\n') |
| exit(1) |
| for code, rate in webdata['rates'].items() : |
| addrate(verbose,'1|{} '+base,code,rate) |
| else: |
| webdata = getjson('https://v6.exchangerate-api.com/v6/'+key+'/latest/'+base) |
| if not webdata['result'] or webdata['result'] != 'success': |
| stderr.write('Currency server error: '+webdata['error-type']+'\n') |
| exit(1) |
| for code, rate in webdata['conversion_rates'].items() : |
| addrate(verbose,'1|{} '+base,code,rate) |
| currency[base][rate_index] = PRIMITIVE |
| return('exchangerate-api.com ('+base+' base)') |
| |
| |
| ####################################################### |
| # |
| # list of valid source names and corresponding functions |
| # |
| |
| sources = { |
| 'exchangerate_api': exchangerate_api, |
| 'floatrates': floatrates, |
| 'eubank' : eubankrates, |
| 'fixer' : fixer, |
| 'openexchangerates': openexchangerates, |
| } |
| |
| default_currency = 'exchangerate_api' |
| |
| ####################################################### |
| # |
| # Argument Processing |
| # |
| |
| ap = ArgumentParser( |
| description="Update currency information for 'units' " |
| "into the specified filenames or the default currency " |
| "file name {} and CPI file name {}. The special " |
| "filename '-' will send either or both files to " |
| "stdout.".format(currency_file,cpi_file), |
| ) |
| |
| ap.add_argument( |
| 'currency_file', |
| default=currency_file, |
| help='the file to update', |
| metavar='currency_file', |
| nargs='?', |
| type=str, |
| ) |
| |
| ap.add_argument( |
| 'cpi_file', |
| default=cpi_file, |
| help='the file to update', |
| metavar='cpi_file', |
| nargs='?', |
| type=str, |
| ) |
| |
| ap.add_argument('-V','--version', |
| action='version', |
| version='%(prog)s version ' + version, |
| help='display units_cur version', |
| ) |
| |
| ap.add_argument('-v','--verbose', |
| action='store_true', |
| help='display details when fetching currency data', |
| ) |
| |
| ap.add_argument('-s','--source',choices=list(sources.keys()), |
| default=default_currency, |
| help='set currency data source (default: {})'.format(default_currency), |
| ) |
| |
| ap.add_argument('-b','--base',default='USD', |
| help='set the base currency (when allowed by source). BASE should be a 3 letter ISO currency code, e.g. USD. The specified currency will be the primitive currency unit used by units. Only the floatrates source supports this option.', |
| ) |
| |
| ap.add_argument('-k','--key',default='', |
| help='set API key for sources that require it' |
| ) |
| |
| ap.add_argument('--blskey',default='', |
| help='set BLS key for fetching CPI data' |
| ) |
| |
| |
| args = ap.parse_args() |
| currency_file = args.currency_file |
| cpi_file = args.cpi_file |
| verbose = args.verbose |
| source = args.source |
| base = args.base |
| apikey = args.key |
| cpikey = args.blskey |
| |
| if base not in currency.keys(): |
| stderr.write('Base currency {} is not a known currency code.\n'.format(base)) |
| exit(1) |
| |
| ######################################################## |
| # |
| # Fetch currency data from specified curerncy source |
| # |
| |
| sourcename = sources[source](verbose,base,apikey) |
| |
| # Delete currencies where we have no rate data |
| for code in list(currency.keys()): |
| if not currency[code][rate_index]: |
| if verbose: |
| stderr.write('No data for {}\n'.format(code)) |
| del currency[code] |
| |
| cnames = [currency[code][0] for code in currency.keys()] |
| crates = [currency[code][1] for code in currency.keys()] |
| |
| codestr = '\n'.join('{:23}{}'. |
| format(code, name) for (code,name) in zip(currency.keys(), cnames)) |
| |
| maxlen = max(len(name) for name in cnames) + 2 |
| |
| ratestr = '\n'.join( |
| '{:{}}{}'.format(name, maxlen, rate) for (name, rate) in zip(cnames, crates) |
| ) |
| |
| ####################################################### |
| # |
| # Get precious metals data and bitcoin |
| # |
| |
| metals = getjson('https://services.packetizer.com/spotprices',{'f':'json'}) |
| bitcoin = getjson('https://services.packetizer.com/btc',{'f':'json'}) |
| |
| metallist = ['']*len(validmetals) |
| for metal, price in metals.items(): |
| if metal in validmetals: |
| metalindex = validmetals.index(metal) |
| if validfloat(price): |
| if not metallist[metalindex]: |
| metallist[validmetals.index(metal)] = '{:19}{} US$/troyounce'.format( |
| metal + 'price', price) |
| elif verbose: |
| stderr.write('Got value "{}" for metal "{}" but ' |
| 'it is already defined\n'.format(price,metal)) |
| else: |
| stderr.write('Got invalid rate "{}" for metal "{}"\n'.format(price,metal)) |
| elif metal != 'date' and verbose: # Don't print a message for the "date" entry |
| stderr.write('Got unknown metal "{}" with value "{}"\n'.format(metal,price)) |
| metalstr = '\n'.join(metallist) |
| |
| if validfloat(bitcoin['usd']): |
| bitcoinstr = '{:{}}{} US$ # From services.packetizer.com/btc\n'.format( |
| 'bitcoin',maxlen,bitcoin['usd']) |
| else: |
| stderr.write('Got invalid bitcoin rate "{}"\n', bitcoint['usd']) |
| bitcointstr='' |
| |
| ####################################################### |
| # |
| # Get CPI data |
| # |
| |
| docpi=False |
| if cpi_file=='-': |
| docpi=True |
| else: |
| try: |
| filedate = getmtime(cpi_file) |
| filedate = date.fromtimestamp(filedate) |
| today = date.today() |
| dmonth = (today.month-filedate.month) + 12*(today.year-filedate.year) |
| if dmonth>1 or (dmonth==1 and today.day>18): |
| docpi=True |
| except FileNotFoundError: |
| docpi=True |
| |
| if docpi: |
| headers = {'Content-type': 'application/json'} |
| yearlist = list(range(date.today().year,1912,-10)) |
| if yearlist[-1]>1912: |
| yearlist.append(1912) |
| cpi=[] |
| lastcpi = 0 |
| query = {"seriesid": ['CUUR0000SA0']} |
| if cpikey: |
| query["registrationkey"]=cpikey |
| ######################################################################## |
| # The api.bls.gov site currently (2024-02-15) resolves to an |
| # IPv4 address which works, and an IPv6 address which does |
| # not. The urllib3 package does not currently implement the |
| # Happy Eyeballs algorithm, nor any other mechanism to re-try |
| # hung connections with alternative addresses returned by DNS. |
| # In the interest of expediency, we temporarily force the |
| # connection to api.bls.gov to only use IPv4; hopefully at |
| # some future date either urllib3 will gain the necessary |
| # features and/or the BLS will fix their configuration so |
| # that all A and AAAA records for api.bls.gov resolve to an |
| # operational server. At that time we can remove the three |
| # references to "requests.packages.urllib3.util.connection.HAS_IPV6" |
| # in this function. |
| ######################################################################## |
| save_rpuucH = requests.packages.urllib3.util.connection.HAS_IPV6 |
| requests.packages.urllib3.util.connection.HAS_IPV6 = False |
| for endyear in range(len(yearlist)-1): |
| query["startyear"]=str(yearlist[endyear+1]+1) |
| query["endyear"]=str(yearlist[endyear]) |
| data = json.dumps(query) |
| p = requests.post('https://api.bls.gov/publicAPI/v2/timeseries/data/', data=data, headers=headers) |
| json_data = json.loads(p.text) |
| if json_data['status']=="REQUEST_NOT_PROCESSED": |
| docpi=False |
| stderr.write("Unable to update CPI data: Exceeded daily threshold for BLS requests\n") |
| break |
| for series in json_data['Results']['series']: |
| for item in series['data']: |
| if not ('M01' <= item['period'] <= 'M12'): |
| continue |
| year = int(item['year']) + int(item['period'][1:])/12 |
| value = item['value'] |
| cpi.append(' '*10 + "{} {} \\".format(year,value)) |
| if lastcpi==0: |
| lastcpi=value |
| lastyear=year |
| firstcpi=value |
| firstyear=year |
| requests.packages.urllib3.util.connection.HAS_IPV6 = save_rpuucH |
| if docpi: # Check again because request may have failed |
| cpi.reverse() |
| cpistr = '\n'.join(cpi) |
| |
| cpi_text = ("""!message Consumer price index data from US BLS, {datestr} |
| |
| UScpi[1] noerror \\ |
| {cpistr} |
| |
| UScpi_now {lastcpi} |
| UScpi_lastdate {lastyear} |
| |
| USdollars_in(date) units=[1;$] domain=[{firstyear},{lastyear}] \\ |
| range=[1,{maxinfl}] \\ |
| US$ UScpi_now / UScpi(date) ;\\ |
| ~UScpi(US$ UScpi_now / USdollars_in) |
| USinflation_since(date) units=[1;1] domain=[{firstyear},{lastyear}] \\ |
| range=[1,{maxinfl}] \\ |
| UScpi_now / UScpi(date) ;\\ |
| ~UScpi(UScpi_now / USinflation_since) |
| |
| """.format(datestr=datestr,cpistr=cpistr,maxinfl=float(lastcpi)/float(firstcpi),lastcpi=lastcpi, |
| firstyear=firstyear,lastyear=lastyear) |
| ).replace('\n', linesep) |
| |
| ####################################################### |
| # |
| # Format output and write the currency file |
| # |
| |
| currency_text = ( |
| """# ISO Currency Codes |
| |
| {codestr} |
| |
| # Currency exchange rates source |
| |
| !message Currency exchange rates from {sourcename} on {datestr} |
| |
| {ratestr} |
| {bitcoinstr} |
| |
| # Precious metals prices from Packetizer (services.packetizer.com/spotprices) |
| |
| {metalstr} |
| |
| """.format(codestr=codestr, datestr=datestr, ratestr=ratestr, metalstr=metalstr, |
| bitcoinstr=bitcoinstr, sourcename=sourcename) |
| ).replace('\n', linesep) |
| |
| |
| ###################################### |
| |
| try: |
| if currency_file == '-': |
| codecs.StreamReader(stdout, codecs.getreader('utf8')).write(currency_text) |
| else: |
| with codecs.open(currency_file, 'w', 'utf8') as of: |
| of.write(currency_text) |
| except IOError as e: |
| stderr.write('Unable to write to output file:\n{}\n'.format(e)) |
| exit(1) |
| |
| if docpi: |
| try: |
| if cpi_file == '-': |
| codecs.StreamReader(stdout, codecs.getreader('utf8')).write(cpi_text) |
| else: |
| with codecs.open(cpi_file, 'w', 'utf8') as of: |
| of.write(cpi_text) |
| except IOError as e: |
| stderr.write('Unable to write to output file:\n{}\n'.format(e)) |
| exit(1) |
| |