Initial commit
diff --git a/units_cur_inst b/units_cur_inst
new file mode 100644
index 0000000..587cd31
--- /dev/null
+++ b/units_cur_inst
@@ -0,0 +1,721 @@
+#!/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)
+