Support different data sources for unite_cur due to disappearance of Yahoo feed

This commit is contained in:
wang--ge 2023-09-18 18:12:40 +08:00
parent 6694b2b30c
commit 20160c3296
2 changed files with 385 additions and 61 deletions

View File

@ -1,23 +1,28 @@
From 9d1129f41f193a47d6791f44f14abe9479999266 Mon Sep 17 00:00:00 2001 From 321eba0f7943a7172dbfce6d470145e48ada6397 Mon Sep 17 00:00:00 2001
From: Kamil Dudka <kdudka@redhat.com> From: wang--ge <wang__ge@126.com>
Date: Wed, 8 Aug 2018 17:42:17 +0200 Date: Mon, 18 Sep 2023 17:23:17 +0800
Subject: [PATCH] units_cur: validate rate data from server Subject: [PATCH] units 2.17 units_cur validate
--- ---
units_cur | 72 ++++++++++++++++++++++++++++++++++++++++++------------- units_cur | 322 +++++++++++++++++++++++++++++++++++++++++-------------
1 file changed, 55 insertions(+), 17 deletions(-) 1 file changed, 245 insertions(+), 77 deletions(-)
diff --git a/units_cur b/units_cur diff --git a/units_cur b/units_cur
index 00281d8..d625570 100755 index 00281d8..70c8d8e 100755
--- a/units_cur --- a/units_cur
+++ b/units_cur +++ b/units_cur
@@ -28,8 +28,12 @@ from __future__ import absolute_import, division, print_function @@ -28,8 +28,17 @@ from __future__ import absolute_import, division, print_function
# #
# #
-version = '4.2' -version = '4.2'
+version = '4.3' +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 +# Version 4.3: 20 July 2018
+# +#
+# Validate rate data from server +# Validate rate data from server
@ -25,22 +30,124 @@ index 00281d8..d625570 100755
# Version 4.2: 18 April 2018 # Version 4.2: 18 April 2018
# #
# Handle case of empty/malformed entry returned from the server # Handle case of empty/malformed entry returned from the server
@@ -55,6 +59,10 @@ from sys import exit, stderr, stdout @@ -55,10 +64,17 @@ from sys import exit, stderr, stdout
outfile_name = 'currency.units' outfile_name = 'currency.units'
+# valid metals +# valid metals
+ +
+validmetals = ['silver','gold','platinum'] +validmetals = ['silver','gold','platinum']
+
+PRIMITIVE = '! # Base unit, the primitive unit of currency'
+ +
# This exchange rate table lists the currency ISO 4217 codes, their # This exchange rate table lists the currency ISO 4217 codes, their
# long text names, and any fixed definitions. If the definition is # long text names, and any fixed definitions. If the definition is
# empty then units_cur will query the server for a value. # empty then units_cur will query the server for a value.
@@ -271,11 +279,19 @@ ap.add_argument('-v','--verbose',
help='display details when fetching currency data',
)
+ +rate_index = 1
currency = OrderedDict([
('ATS', ['austriaschilling', '1|13.7603 euro']),
('BEF', ['belgiumfranc', '1|40.3399 euro']),
@@ -83,13 +99,14 @@ currency = OrderedDict([
('BGN', ['bulgarialev', '1|1.9558 euro']),
('BAM', ['bosniaconvertiblemark','germanymark']),
('KMF', ['comorosfranc', '1|491.96775 euro']),
- ('XOF', ['westafricanfranc', '1|655.957 euro']),
+ ('XOF', ['westafricafranc', '1|655.957 euro']),
('XPF', ['cfpfranc', '1|119.33 euro']),
- ('XAF', ['centralafricancfafranc','1|655.957 euro']),
+ ('XAF', ['centralafricacfafranc','1|655.957 euro']),
('AED', ['uaedirham','']),
('AFN', ['afghanafghani','']),
('ALL', ['albanialek','']),
('AMD', ['armeniadram','']),
+ ('ANG', ['antillesguilder','']),
('AOA', ['angolakwanza','']),
('ARS', ['argentinapeso','']),
('AUD', ['australiadollar','']),
@@ -109,7 +126,7 @@ currency = OrderedDict([
('BTN', ['bhutanngultrum','']),
('BWP', ['botswanapula','']),
('BYN', ['belarusruble','']),
- ('BYR', ['oldbelarusruble','10000 BYN']),
+ ('BYR', ['oldbelarusruble','1|10000 BYN']),
('BZD', ['belizedollar','']),
('CAD', ['canadadollar','']),
('CDF', ['drcfranccongolais','']),
@@ -127,7 +144,7 @@ currency = OrderedDict([
('DZD', ['algeriadinar','']),
('EGP', ['egyptpound','']),
('ERN', ['eritreanakfa','']),
- ('ETB', ['ethiopianbirr','']),
+ ('ETB', ['ethiopiabirr','']),
('EUR', ['euro','']),
('FJD', ['fijidollar','']),
('FKP', ['falklandislandspound','']),
@@ -164,10 +181,9 @@ currency = OrderedDict([
('KZT', ['kazakhstantenge','']),
('LAK', ['laokip','']),
('LBP', ['lebanonpound','']),
- ('LKR', ['srilankanrupee','']),
+ ('LKR', ['srilankarupee','']),
('LRD', ['liberiadollar','']),
- ('LTL', ['lithuanialita','']),
- ('LVL', ['latvialat','']),
+ ('LSL', ['lesotholoti','']),
('LYD', ['libyadinar','']),
('MAD', ['moroccodirham','']),
('MDL', ['moldovaleu','']),
@@ -176,13 +192,14 @@ currency = OrderedDict([
('MMK', ['myanmarkyat','']),
('MNT', ['mongoliatugrik','']),
('MOP', ['macaupataca','']),
- ('MRO', ['mauritaniaouguiya','']),
+ ('MRO', ['mauritaniaoldouguiya','1|10 MRU']),
+ ('MRU', ['mauritaniaouguiya', '']),
('MUR', ['mauritiusrupee','']),
('MVR', ['maldiverufiyaa','']),
('MWK', ['malawikwacha','']),
('MXN', ['mexicopeso','']),
('MYR', ['malaysiaringgit','']),
- ('MZN', ['mozambicanmetical','']),
+ ('MZN', ['mozambiquemetical','']),
('NAD', ['namibiadollar','']),
('NGN', ['nigerianaira','']),
('NIO', ['nicaraguacordobaoro','']),
@@ -212,7 +229,9 @@ currency = OrderedDict([
('SLL', ['sierraleoneleone','']),
('SOS', ['somaliaschilling','']),
('SRD', ['surinamedollar','']),
- ('STD', ['saotome&principedobra','']),
+ ('SSP', ['southsudanpound','']),
+ ('STD', ['saotome&principeolddobra','']),
+ ('STN', ['saotome&principedobra','']),
('SVC', ['elsalvadorcolon','']),
('SYP', ['syriapound','']),
('SZL', ['swazilandlilangeni','']),
@@ -227,15 +246,15 @@ currency = OrderedDict([
('TZS', ['tanzaniashilling','']),
('UAH', ['ukrainehryvnia','']),
('UGX', ['ugandaschilling','']),
- ('USD', ['unitedstatesdollar', 'US$']),
+ ('USD', ['US$', '']),
('UYU', ['uruguaypeso','']),
('UZS', ['uzbekistansum','']),
- ('VEF', ['venezuelabolivar','']),
- ('VEB', ['venezuelaoldbolivar', '1000 VEF']),
+ ('VEF', ['venezuelabolivarfuerte','']),
+ ('VES', ['venezuelabolivarsoberano','']),
('VND', ['vietnamdong','']),
('VUV', ['vanuatuvatu','']),
('WST', ['samoatala','']),
- ('XAF', ['centralafricancfafranc','']),
+ ('XAF', ['centralafricacfafranc','']),
('XCD', ['eastcaribbeandollar','']),
('XDR', ['specialdrawingrights','']),
('YER', ['yemenrial','']),
@@ -244,6 +263,147 @@ currency = OrderedDict([
('ZWL', ['zimbabwedollar','']),
])
+def validfloat(x): +def validfloat(x):
+ try: + try:
+ float(x) + float(x)
@ -48,56 +155,246 @@ index 00281d8..d625570 100755
+ except ValueError: + except ValueError:
+ return False + return False
+ +
outfile_name = ap.parse_args().output_file +def addrate(verbose,form,code,rate):
verbose = ap.parse_args().verbose + 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)')
+
+#######################################################
+#
+# list of valid source names and corresponding functions
+#
+
+sources = {
+ 'floatrates': floatrates,
+ 'eubank' : eubankrates,
+ 'fixer' : fixer,
+ 'openexchangerates': openexchangerates,
+}
+
+#######################################################
+#
+# Argument Processing
+#
+
ap = ArgumentParser(
description="Update currency information for 'units' "
"into the specified filename or if no filename is "
@@ -271,64 +431,43 @@ ap.add_argument('-v','--verbose',
help='display details when fetching currency data',
)
try: -outfile_name = ap.parse_args().output_file
-verbose = ap.parse_args().verbose
+ap.add_argument('-s','--source',choices=list(sources.keys()),
+ default='floatrates',
+ help='set currency data source',
+)
-try:
- res = requests.get('http://finance.yahoo.com/webservice/v1/symbols' - res = requests.get('http://finance.yahoo.com/webservice/v1/symbols'
+ res = requests.get('https://finance.yahoo.com/webservice/v1/symbols' - '/allcurrencies/quote?format=json')
'/allcurrencies/quote?format=json') - res.raise_for_status()
res.raise_for_status() - webdata = res.json()['list']['resources']
webdata = res.json()['list']['resources'] -except requests.exceptions.RequestException as e:
@@ -299,10 +315,16 @@ for data in webdata: - stderr.write('Error connecting to currency server:\n{}.\n'.
stderr.write('Got unknown currency with code {}\n'.format(code)) - format(e))
else: +ap.add_argument('-b','--base',default='USD',
if not currency[code][rate_index]: + 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'
+)
+
+args = ap.parse_args()
+outfile_name = args.output_file
+verbose = args.verbose
+source = args.source
+base = args.base
+apikey = args.key
+
+if base not in currency.keys():
+ stderr.write('Base currency {} is not a known currency code.\n'.format(base))
exit(1)
-
-rate_index = 1
-for data in webdata:
- entry = data['resource']['fields']
- if 'price' not in entry or 'symbol' not in entry: # Skip empty/bad entries
- if verbose:
- stderr.write('Got bad entry from server: '+str(entry)+'\n')
- else:
- rate = entry['price']
- code = entry['symbol'][0:3]
- 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]:
- currency[code][rate_index] = '1|{} US$'.format(rate) - currency[code][rate_index] = '1|{} US$'.format(rate)
+ if validfloat(rate): - elif verbose:
+ currency[code][rate_index] = '1|{} US$'.format(rate)
+ else:
+ stderr.write('Got invalid rate "{}" for currency "{}"\n'.format(
+ rate, code))
elif verbose:
- stderr.write('Got value "{}" for currency "{}" but ' - stderr.write('Got value "{}" for currency "{}" but '
- 'it is already defined\n'.format(rate, code)) - 'it is already defined\n'.format(rate, code))
+ if currency[code][rate_index] != '1|{} US$'.format(rate): +########################################################
+ stderr.write('Got value "{}" for currency "{}" but ' +#
+ 'it is already defined as {}\n'.format(rate, code, +# Fetch currency data from specified curerncy source
+ currency[code][rate_index])) +#
-
+sourcename = sources[source](verbose,base,apikey)
+
# Delete currencies where we have no rate data # Delete currencies where we have no rate data
@@ -313,17 +335,15 @@ for code in currency.keys(): -for code in currency.keys():
+for code in list(currency.keys()):
if not currency[code][rate_index]:
if verbose:
- stderr.write('No data for {}'.format(code))
+ stderr.write('No data for {}\n'.format(code))
del currency[code] del currency[code]
-
try: -try:
- req = requests.get('http://services.packetizer.com/spotprices/?f=json') - req = requests.get('http://services.packetizer.com/spotprices/?f=json')
+ req = requests.get('https://services.packetizer.com/spotprices/?f=json') - req.raise_for_status()
req.raise_for_status() - metals = req.json()
metals = req.json() -except requests.exceptions.RequestException as e:
except requests.exceptions.RequestException as e: - stderr.write('Error connecting to spotprices server:\n{}\n'.format(e))
stderr.write('Error connecting to spotprices server:\n{}\n'.format(e)) - exit(1)
exit(1) -
-del metals['date'] -del metals['date']
- -
try: -try:
- req = requests.get('http://services.packetizer.com/btc/?f=json') - req = requests.get('http://services.packetizer.com/btc/?f=json')
+ req = requests.get('https://services.packetizer.com/btc/?f=json') - req.raise_for_status()
req.raise_for_status() - bitcoin = req.json()
bitcoin = req.json() -except requests.exceptions.RequestException as e:
except requests.exceptions.RequestException as e: - stderr.write('Error connecting to bitcoin server:\n{}\n'.format(e))
@@ -344,13 +364,31 @@ ratestr = '\n'.join( - exit(1)
cnames = [currency[code][0] for code in currency.keys()]
crates = [currency[code][1] for code in currency.keys()]
@@ -336,40 +475,69 @@ crates = [currency[code][1] for code in currency.keys()]
codestr = '\n'.join('{:23}{}'.
format(code, name) for (code,name) in zip(currency.keys(), cnames))
-datestr = date.today().isoformat()
-
maxlen = max(len(name) for name in cnames) + 2
ratestr = '\n'.join(
'{:{}}{}'.format(name, maxlen, rate) for (name, rate) in zip(cnames, crates) '{:{}}{}'.format(name, maxlen, rate) for (name, rate) in zip(cnames, crates)
) )
@ -105,8 +402,15 @@ index 00281d8..d625570 100755
- metal + 'price', - metal + 'price',
- price, - price,
- ) for metal, price in metals.items()) - ) for metal, price in metals.items())
- +#######################################################
+#
+# Get precious metals data and bitcoin
+#
-bitcoinstr = '{:{}}{} US$ # From services.packetizer.com/btc\n'.format( -bitcoinstr = '{:{}}{} US$ # From services.packetizer.com/btc\n'.format(
+metals = getjson('https://services.packetizer.com/spotprices',{'f':'json'})
+bitcoin = getjson('https://services.packetizer.com/btc',{'f':'json'})
+
+metallist = ['']*len(validmetals) +metallist = ['']*len(validmetals)
+for metal, price in metals.items(): +for metal, price in metals.items():
+ if metal in validmetals: + if metal in validmetals:
@ -119,10 +423,9 @@ index 00281d8..d625570 100755
+ stderr.write('Got value "{}" for metal "{}" but ' + stderr.write('Got value "{}" for metal "{}" but '
+ 'it is already defined\n'.format(price,metal)) + 'it is already defined\n'.format(price,metal))
+ else: + else:
+ stderr.write('Got invalid rate "{}" for metal "{}"\n'.format( + stderr.write('Got invalid rate "{}" for metal "{}"\n'.format(price,metal))
+ price, metal))
+ elif metal != 'date' and verbose: # Don't print a message for the "date" entry + elif metal != 'date' and verbose: # Don't print a message for the "date" entry
+ stderr.write('Got unknown metal "{}" with value "{}"\n',metal,price) + stderr.write('Got unknown metal "{}" with value "{}"\n'.format(metal,price))
+metalstr = '\n'.join(metallist) +metalstr = '\n'.join(metallist)
+ +
+if validfloat(bitcoin['usd']): +if validfloat(bitcoin['usd']):
@ -131,11 +434,27 @@ index 00281d8..d625570 100755
+else: +else:
+ stderr.write('Got invalid bitcoin rate "{}"\n', bitcoint['usd']) + stderr.write('Got invalid bitcoin rate "{}"\n', bitcoint['usd'])
+ bitcointstr='' + bitcointstr=''
+ +
+#######################################################
+#
+# Format output and write the currency file
+#
+datestr = date.today().isoformat()
+
outstr = ( outstr = (
"""# ISO Currency Codes """# ISO Currency Codes
@@ -366,9 +404,9 @@ outstr = (
{codestr}
-# Currency exchange rates from Yahoo Finance (finance.yahoo.com)
+# Currency exchange rates source
-!message Currency exchange rates from finance.yahoo.com on {datestr}
+!message Currency exchange rates from {sourcename} on {datestr}
{ratestr}
{bitcoinstr}
# Precious metals prices from Packetizer (services.packetizer.com/spotprices) # Precious metals prices from Packetizer (services.packetizer.com/spotprices)
@ -143,10 +462,12 @@ index 00281d8..d625570 100755
+{metalstr} +{metalstr}
-""".format(codestr=codestr, datestr=datestr, ratestr=ratestr, ozzystr=ozzystr, -""".format(codestr=codestr, datestr=datestr, ratestr=ratestr, ozzystr=ozzystr,
- bitcoinstr=bitcoinstr)
+""".format(codestr=codestr, datestr=datestr, ratestr=ratestr, metalstr=metalstr, +""".format(codestr=codestr, datestr=datestr, ratestr=ratestr, metalstr=metalstr,
bitcoinstr=bitcoinstr) + bitcoinstr=bitcoinstr, sourcename=sourcename)
).replace('\n', linesep) ).replace('\n', linesep)
try:
-- --
2.17.1 2.33.0

View File

@ -1,6 +1,6 @@
Name: units Name: units
Version: 2.17 Version: 2.17
Release: 9 Release: 10
Summary: A utility for converting amounts from one unit to another Summary: A utility for converting amounts from one unit to another
License: GPLv3+ License: GPLv3+
URL: https://www.gnu.org/software/units/units.html URL: https://www.gnu.org/software/units/units.html
@ -68,6 +68,9 @@ fi
%{_mandir}/man1/* %{_mandir}/man1/*
%changelog %changelog
* Mon Sep 18 2023 Ge Wang <wang__ge@126.com> - 2.17-10
- Support different data sources for units_cur due to disappearance of Yahoo feed.
* Thu Sep 07 2023 Ge Wang <wang__ge@126.com> - 2.17-9 * Thu Sep 07 2023 Ge Wang <wang__ge@126.com> - 2.17-9
- Add install requirement - Add install requirement