blob: 259d4dd351883576a95c82daa68290045539aaf4 [file] [log] [blame]
swissChili729acd52024-03-05 11:52:45 -05001#!/usr/bin/python
2#
3# units_cur for units, a program for updated currency exchange rates
4#
5# Copyright (C) 2017-2018, 2022
6# Free Software Foundation, Inc
7#
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 3 of the License, or
11# (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
17#
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21#
22#
23# This program was written by Adrian Mariano (adrianm@gnu.org)
24#
25
26# For Python 2 & 3 compatibility
27from __future__ import absolute_import, division, print_function
28#
29#
30
31version = '5.0'
32
33# Version 5.0:
34#
35# Rewrite to support multiple different data sources due to disappearance
36# of the Yahoo feed. Includes support for base currency selection.
37#
38# Version 4.3: 20 July 2018
39#
40# Validate rate data from server
41#
42# Version 4.2: 18 April 2018
43#
44# Handle case of empty/malformed entry returned from the server
45#
46# Version 4.1: 30 October 2017
47#
48# Fixed to include USD in the list of currency codes.
49#
50# Version 4: 2 October 2017
51#
52# Complete rewrite to use Yahoo YQL API due to removal of TimeGenie RSS feed.
53# Switched to requests library using JSON. One program now runs under
54# Python 2 or Python 3. Thanks to Ray Hamel for some help with this update.
55
56# Normal imports
57import requests
58import codecs
59import json
60from argparse import ArgumentParser
61from collections import OrderedDict
62from datetime import date
63from os import linesep
64from os.path import getmtime
65from sys import exit, stderr, stdout
66
67datestr = date.today().isoformat()
68
69output_dir = ''
70
71currency_file = output_dir + 'currency.units'
72cpi_file = output_dir + 'cpi.units'
73
74# valid metals
75
76validmetals = ['silver','gold','platinum']
77
78PRIMITIVE = '! # Base unit, the primitive unit of currency'
79
80# This exchange rate table lists the currency ISO 4217 codes, their
81# long text names, and any fixed definitions. If the definition is
82# empty then units_cur will query the server for a value.
83
84rate_index = 1
85currency = OrderedDict([
86 ('ATS', ['austriaschilling', '1|13.7603 euro']),
87 ('BEF', ['belgiumfranc', '1|40.3399 euro']),
88 ('CYP', ['cypruspound', '1|0.585274 euro']),
89 ('EEK', ['estoniakroon', '1|15.6466 euro # Equal to 1|8 germanymark']),
90 ('FIM', ['finlandmarkka', '1|5.94573 euro']),
91 ('FRF', ['francefranc', '1|6.55957 euro']),
92 ('DEM', ['germanymark', '1|1.95583 euro']),
93 ('GRD', ['greecedrachma', '1|340.75 euro']),
94 ('IEP', ['irelandpunt', '1|0.787564 euro']),
95 ('ITL', ['italylira', '1|1936.27 euro']),
96 ('LVL', ['latvialats', '1|0.702804 euro']),
97 ('LTL', ['lithuanialitas', '1|3.4528 euro']),
98 ('LUF', ['luxembourgfranc', '1|40.3399 euro']),
99 ('MTL', ['maltalira', '1|0.4293 euro']),
100 ('SKK', ['slovakiakoruna', '1|30.1260 euro']),
101 ('SIT', ['sloveniatolar', '1|239.640 euro']),
102 ('ESP', ['spainpeseta', '1|166.386 euro']),
103 ('NLG', ['netherlandsguilder','1|2.20371 euro']),
104 ('PTE', ['portugalescudo', '1|200.482 euro']),
105 ('CVE', ['capeverdeescudo', '1|110.265 euro']),
106 ('BGN', ['bulgarialev', '1|1.9558 euro']),
107 ('BAM', ['bosniaconvertiblemark','germanymark']),
108 ('KMF', ['comorosfranc', '1|491.96775 euro']),
109 ('XOF', ['westafricafranc', '1|655.957 euro']),
110 ('XPF', ['cfpfranc', '1|119.33 euro']),
111 ('XAF', ['centralafricacfafranc','1|655.957 euro']),
112 ('AED', ['uaedirham','']),
113 ('AFN', ['afghanistanafghani','']),
114 ('ALL', ['albanialek','']),
115 ('AMD', ['armeniadram','']),
116 ('ANG', ['antillesguilder','']),
117 ('AOA', ['angolakwanza','']),
118 ('ARS', ['argentinapeso','']),
119 ('AUD', ['australiadollar','']),
120 ('AWG', ['arubaflorin','']),
121 ('AZN', ['azerbaijanmanat','']),
122 ('BAM', ['bosniaconvertiblemark','']),
123 ('BBD', ['barbadosdollar','']),
124 ('BDT', ['bangladeshtaka','']),
125 ('BGN', ['bulgarialev','']),
126 ('BHD', ['bahraindinar','']),
127 ('BIF', ['burundifranc','']),
128 ('BMD', ['bermudadollar','']),
129 ('BND', ['bruneidollar','']),
130 ('BOB', ['boliviaboliviano','']),
131 ('BRL', ['brazilreal','']),
132 ('BSD', ['bahamasdollar','']),
133 ('BTN', ['bhutanngultrum','']),
134 ('BWP', ['botswanapula','']),
135 ('BYN', ['belarusruble','']),
136 ('BYR', ['oldbelarusruble','1|10000 BYN']),
137 ('BZD', ['belizedollar','']),
138 ('CAD', ['canadadollar','']),
139 ('CDF', ['drcfranccongolais','']),
140 ('CHF', ['swissfranc','']),
141 ('CLP', ['chilepeso','']),
142 ('CNY', ['chinayuan','']),
143 ('COP', ['colombiapeso','']),
144 ('CRC', ['costaricacolon','']),
145 ('CUP', ['cubapeso','']),
146 ('CVE', ['capeverdeescudo','']),
147 ('CZK', ['czechiakoruna','']),
148 ('DJF', ['djiboutifranc','']),
149 ('DKK', ['denmarkkrone','']),
150 ('DOP', ['dominicanrepublicpeso','']),
151 ('DZD', ['algeriadinar','']),
152 ('EGP', ['egyptpound','']),
153 ('ERN', ['eritreanakfa','']),
154 ('ETB', ['ethiopiabirr','']),
155 ('EUR', ['euro','']),
156 ('FJD', ['fijidollar','']),
157 ('FKP', ['falklandislandspound','']),
158 ('GBP', ['ukpound','']),
159 ('GEL', ['georgialari','']),
160 ('GHS', ['ghanacedi','']),
161 ('GIP', ['gibraltarpound','']),
162 ('GMD', ['gambiadalasi','']),
163 ('GNF', ['guineafranc','']),
164 ('GTQ', ['guatemalaquetzal','']),
165 ('GYD', ['guyanadollar','']),
166 ('HKD', ['hongkongdollar','']),
167 ('HNL', ['honduraslempira','']),
168 ('HRK', ['croatiakuna','']),
169 ('HTG', ['haitigourde','']),
170 ('HUF', ['hungaryforint','']),
171 ('IDR', ['indonesiarupiah','']),
172 ('ILS', ['israelnewshekel','']),
173 ('INR', ['indiarupee','']),
174 ('IQD', ['iraqdinar','']),
175 ('IRR', ['iranrial','']),
176 ('ISK', ['icelandkrona','']),
177 ('JMD', ['jamaicadollar','']),
178 ('JOD', ['jordandinar','']),
179 ('JPY', ['japanyen','']),
180 ('KES', ['kenyaschilling','']),
181 ('KGS', ['kyrgyzstansom','']),
182 ('KHR', ['cambodiariel','']),
183 ('KMF', ['comorosfranc','']),
184 ('KPW', ['northkoreawon','']),
185 ('KRW', ['southkoreawon','']),
186 ('KWD', ['kuwaitdinar','']),
187 ('KYD', ['caymanislandsdollar','']),
188 ('KZT', ['kazakhstantenge','']),
189 ('LAK', ['laokip','']),
190 ('LBP', ['lebanonpound','']),
191 ('LKR', ['srilankarupee','']),
192 ('LRD', ['liberiadollar','']),
193 ('LSL', ['lesotholoti','']),
194 ('LYD', ['libyadinar','']),
195 ('MAD', ['moroccodirham','']),
196 ('MDL', ['moldovaleu','']),
197 ('MGA', ['madagascarariary','']),
198 ('MKD', ['macedoniadenar','']),
199 ('MMK', ['myanmarkyat','']),
200 ('MNT', ['mongoliatugrik','']),
201 ('MOP', ['macaupataca','']),
202 ('MRO', ['mauritaniaoldouguiya','1|10 MRU']),
203 ('MRU', ['mauritaniaouguiya', '']),
204 ('MUR', ['mauritiusrupee','']),
205 ('MVR', ['maldiverufiyaa','']),
206 ('MWK', ['malawikwacha','']),
207 ('MXN', ['mexicopeso','']),
208 ('MYR', ['malaysiaringgit','']),
209 ('MZN', ['mozambiquemetical','']),
210 ('NAD', ['namibiadollar','']),
211 ('NGN', ['nigerianaira','']),
212 ('NIO', ['nicaraguacordobaoro','']),
213 ('NOK', ['norwaykrone','']),
214 ('NPR', ['nepalrupee','']),
215 ('NZD', ['newzealanddollar','']),
216 ('OMR', ['omanrial','']),
217 ('PAB', ['panamabalboa','']),
218 ('PEN', ['perunuevosol','']),
219 ('PGK', ['papuanewguineakina','']),
220 ('PHP', ['philippinepeso','']),
221 ('PKR', ['pakistanrupee','']),
222 ('PLN', ['polandzloty','']),
223 ('PYG', ['paraguayguarani','']),
224 ('QAR', ['qatarrial','']),
225 ('RON', ['romanianewlei','']),
226 ('RSD', ['serbiadinar','']),
227 ('RUB', ['russiaruble','']),
228 ('RWF', ['rwandafranc','']),
229 ('SAR', ['saudiarabiariyal','']),
230 ('SBD', ['solomonislandsdollar','']),
231 ('SCR', ['seychellesrupee','']),
232 ('SDG', ['sudanpound','']),
233 ('SEK', ['swedenkrona','']),
234 ('SGD', ['singaporedollar','']),
235 ('SHP', ['sainthelenapound','']),
236# ('SLL', ['sierraleoneoldleone','1|1000 SLE']),
237# ('SLE', ['sierraleoneleone','']),
238 ('SOS', ['somaliaschilling','']),
239 ('SRD', ['surinamedollar','']),
240 ('SSP', ['southsudanpound','']),
241 ('STD', ['saotome&principeolddobra','']),
242 ('STN', ['saotome&principedobra','']),
243# ('SVC', ['elsalvadorcolon','']), # eliminated in 2001
244 ('SYP', ['syriapound','']),
245 ('SZL', ['swazilandlilangeni','']),
246 ('THB', ['thailandbaht','']),
247 ('TJS', ['tajikistansomoni','']),
248 ('TMT', ['turkmenistanmanat','']),
249 ('TND', ['tunisiadinar','']),
250 ('TOP', ["tongapa'anga",'']),
251 ('TRY', ['turkeylira','']),
252 ('TTD', ['trinidadandtobagodollar','']),
253 ('TWD', ['taiwandollar','']),
254 ('TZS', ['tanzaniashilling','']),
255 ('UAH', ['ukrainehryvnia','']),
256 ('UGX', ['ugandaschilling','']),
257 ('USD', ['US$', '']),
258 ('UYU', ['uruguaypeso','']),
259 ('UZS', ['uzbekistansum','']),
260 ('VEF', ['venezuelabolivarfuerte','']),
261 ('VES', ['venezuelabolivarsoberano','']),
262 ('VND', ['vietnamdong','']),
263 ('VUV', ['vanuatuvatu','']),
264 ('WST', ['samoatala','']),
265 ('XAF', ['centralafricacfafranc','']),
266 ('XCD', ['eastcaribbeandollar','']),
267 ('XDR', ['specialdrawingrights','']),
268 ('YER', ['yemenrial','']),
269 ('ZAR', ['southafricarand','']),
270 ('ZMW', ['zambiakwacha','']),
271 ('ZWL', ['zimbabwedollar','']),
272 ('FOK', ['faroeislandskróna','DKK']),
273 ('GGP', ['guernseypound', 'GBP']),
274 ('IMP', ['isleofmanpound','GBP']),
275 ('JEP', ['jerseypound','GBP']),
276 ('KID', ['kiribatidollar','AUD']),
277 ('TVD', ['tuvaludollar','AUD']),
278])
279
280
281def validfloat(x):
282 try:
283 float(x)
284 return True
285 except ValueError:
286 return False
287
288def addrate(verbose,form,code,rate):
289 if code not in currency.keys():
290 if (verbose):
291 stderr.write('Got unknown currency with code {}\n'.format(code))
292 else:
293 if not currency[code][rate_index]:
294 if validfloat(rate):
295 currency[code][rate_index] = form.format(rate)
296 else:
297 stderr.write('Got invalid rate "{}" for currency "{}"\n'.format(
298 rate, code))
299 elif verbose:
300 if currency[code][rate_index] != form.format(rate):
301 stderr.write('Got value "{}" for currency "{}" but '
302 'it is already defined as {}\n'.format(rate, code,
303 currency[code][rate_index]))
304
305def getjson(address,args=None):
306 try:
307 res = requests.get(address,args)
308 res.raise_for_status()
309 return(res.json())
310 except requests.exceptions.RequestException as e:
311 stderr.write('Error connecting to currency server:\n{}.\n'.format(e))
312 exit(1)
313
314########################################################
315#
316# Connect to floatrates for currency update
317#
318
319def floatrates(verbose,base,dummy):
320 webdata = getjson('https://www.floatrates.com/daily/'+base+'.json')
321 for index in webdata:
322 entry = webdata[index]
323 if 'rate' not in entry or 'code' not in entry: # Skip empty/bad entries
324 if verbose:
325 stderr.write('Got bad entry from server: '+str(entry)+'\n')
326 else:
327 addrate(verbose,'{} '+base,entry['code'],entry['inverseRate'])
328 currency[base][rate_index] = PRIMITIVE
329 return('FloatRates ('+base+' base)')
330
331########################################################
332#
333# Connect to European central bank site
334#
335
336def eubankrates(verbose,base,dummy):
337 if verbose and base!='EUR':
338 stderr.write('European bank uses euro for base currency. Specified base {} ignored.\n'.format(base))
339 import xml.etree.ElementTree as ET
340 try:
341 res=requests.get('https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml')
342 res.raise_for_status()
343 data = ET.fromstring(res.content)[2][0]
344 except requests.exceptions.RequestException as e:
345 stderr.write('Error connecting to currency server:\n{}.\n'.
346 format(e))
347 exit(1)
348 for entry in data.iter():
349 if entry.get('time'):
350 continue
351 rate = entry.get('rate')
352 code = entry.get('currency')
353 if not rate or not code: # Skip empty/bad entries
354 if verbose:
355 stderr.write('Got bad entry from server, code {} and rate {}\n'.format(code,rate))
356 else:
357 addrate(verbose,'1|{} euro', code, rate)
358 currency['EUR'][rate_index]=PRIMITIVE
359 return('the European Central Bank (euro base)')
360
361########################################################
362#
363# Connect to fixer.io (requires API key)
364#
365# Free API key does not allow changing base currency
366# With free key only euro base is supported, and https is not allowed
367#
368
369def fixer(verbose,base,key):
370 if not key:
371 stderr.write('API key required for this source\n')
372 exit(1)
373 if verbose and base!='EUR':
374 stderr.write('Fixer uses euro for base currency. Specified base {} ignored.\n'.format(base))
375 webdata = getjson('http://data.fixer.io/api/latest', {'access_key':key})
376 if not webdata['success']:
377 stderr.write('Currency server error: '+webdata['error']['info'])
378 exit(1)
379 for code in webdata['rates']:
380 addrate(verbose,'1|{} euro', code, webdata['rates'][code])
381 currency['EUR'][rate_index] = PRIMITIVE
382 return('Fixer (euro base)')
383
384########################################################
385#
386# Connect to openexchangerates (requires API key)
387#
388# Free API key does not allow changing the base currency
389#
390
391def openexchangerates(verbose,base,key):
392 if not key:
393 stderr.write('API key required for this source\n')
394 exit(1)
395 if verbose and base!='USD':
396 stderr.write('Open Exchange Rates uses US dollar for base currency. Specified base {} ignored.\n'.format(base))
397 webdata = getjson('https://openexchangerates.org/api/latest.json',
398 {'app_id':key}
399 )
400 for code in webdata['rates']:
401 addrate(verbose,'1|{} US$', code, webdata['rates'][code])
402 currency['USD'][rate_index] = PRIMITIVE
403 return('open exchange rates (USD base)')
404
405########################################################
406#
407# Connect to exchangerate-api.com
408#
409# The open access endpoint gives updates once per day.
410# User can optionally supply a paid account's API key to get newer data and more precision.
411# Base currency can be changed with both open and paid versions of the API.
412#
413
414def exchangerate_api(verbose,base,key):
415 if not key:
416 webdata = getjson('https://open.er-api.com/v6/latest/'+base)
417 if not webdata['result'] or webdata['result'] != 'success':
418 stderr.write('Currency server error: '+webdata['error-type']+'\n')
419 exit(1)
420 for code, rate in webdata['rates'].items() :
421 addrate(verbose,'1|{} '+base,code,rate)
422 else:
423 webdata = getjson('https://v6.exchangerate-api.com/v6/'+key+'/latest/'+base)
424 if not webdata['result'] or webdata['result'] != 'success':
425 stderr.write('Currency server error: '+webdata['error-type']+'\n')
426 exit(1)
427 for code, rate in webdata['conversion_rates'].items() :
428 addrate(verbose,'1|{} '+base,code,rate)
429 currency[base][rate_index] = PRIMITIVE
430 return('exchangerate-api.com ('+base+' base)')
431
432
433#######################################################
434#
435# list of valid source names and corresponding functions
436#
437
438sources = {
439 'exchangerate_api': exchangerate_api,
440 'floatrates': floatrates,
441 'eubank' : eubankrates,
442 'fixer' : fixer,
443 'openexchangerates': openexchangerates,
444}
445
446default_currency = 'exchangerate_api'
447
448#######################################################
449#
450# Argument Processing
451#
452
453ap = ArgumentParser(
454 description="Update currency information for 'units' "
455 "into the specified filenames or the default currency "
456 "file name {} and CPI file name {}. The special "
457 "filename '-' will send either or both files to "
458 "stdout.".format(currency_file,cpi_file),
459)
460
461ap.add_argument(
462 'currency_file',
463 default=currency_file,
464 help='the file to update',
465 metavar='currency_file',
466 nargs='?',
467 type=str,
468)
469
470ap.add_argument(
471 'cpi_file',
472 default=cpi_file,
473 help='the file to update',
474 metavar='cpi_file',
475 nargs='?',
476 type=str,
477)
478
479ap.add_argument('-V','--version',
480 action='version',
481 version='%(prog)s version ' + version,
482 help='display units_cur version',
483)
484
485ap.add_argument('-v','--verbose',
486 action='store_true',
487 help='display details when fetching currency data',
488)
489
490ap.add_argument('-s','--source',choices=list(sources.keys()),
491 default=default_currency,
492 help='set currency data source (default: {})'.format(default_currency),
493)
494
495ap.add_argument('-b','--base',default='USD',
496 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.',
497)
498
499ap.add_argument('-k','--key',default='',
500 help='set API key for sources that require it'
501)
502
503ap.add_argument('--blskey',default='',
504 help='set BLS key for fetching CPI data'
505)
506
507
508args = ap.parse_args()
509currency_file = args.currency_file
510cpi_file = args.cpi_file
511verbose = args.verbose
512source = args.source
513base = args.base
514apikey = args.key
515cpikey = args.blskey
516
517if base not in currency.keys():
518 stderr.write('Base currency {} is not a known currency code.\n'.format(base))
519 exit(1)
520
521########################################################
522#
523# Fetch currency data from specified curerncy source
524#
525
526sourcename = sources[source](verbose,base,apikey)
527
528# Delete currencies where we have no rate data
529for code in list(currency.keys()):
530 if not currency[code][rate_index]:
531 if verbose:
532 stderr.write('No data for {}\n'.format(code))
533 del currency[code]
534
535cnames = [currency[code][0] for code in currency.keys()]
536crates = [currency[code][1] for code in currency.keys()]
537
538codestr = '\n'.join('{:23}{}'.
539 format(code, name) for (code,name) in zip(currency.keys(), cnames))
540
541maxlen = max(len(name) for name in cnames) + 2
542
543ratestr = '\n'.join(
544 '{:{}}{}'.format(name, maxlen, rate) for (name, rate) in zip(cnames, crates)
545 )
546
547#######################################################
548#
549# Get precious metals data and bitcoin
550#
551
552metals = getjson('https://services.packetizer.com/spotprices',{'f':'json'})
553bitcoin = getjson('https://services.packetizer.com/btc',{'f':'json'})
554
555metallist = ['']*len(validmetals)
556for metal, price in metals.items():
557 if metal in validmetals:
558 metalindex = validmetals.index(metal)
559 if validfloat(price):
560 if not metallist[metalindex]:
561 metallist[validmetals.index(metal)] = '{:19}{} US$/troyounce'.format(
562 metal + 'price', price)
563 elif verbose:
564 stderr.write('Got value "{}" for metal "{}" but '
565 'it is already defined\n'.format(price,metal))
566 else:
567 stderr.write('Got invalid rate "{}" for metal "{}"\n'.format(price,metal))
568 elif metal != 'date' and verbose: # Don't print a message for the "date" entry
569 stderr.write('Got unknown metal "{}" with value "{}"\n'.format(metal,price))
570metalstr = '\n'.join(metallist)
571
572if validfloat(bitcoin['usd']):
573 bitcoinstr = '{:{}}{} US$ # From services.packetizer.com/btc\n'.format(
574 'bitcoin',maxlen,bitcoin['usd'])
575else:
576 stderr.write('Got invalid bitcoin rate "{}"\n', bitcoint['usd'])
577 bitcointstr=''
578
579#######################################################
580#
581# Get CPI data
582#
583
584docpi=False
585if cpi_file=='-':
586 docpi=True
587else:
588 try:
589 filedate = getmtime(cpi_file)
590 filedate = date.fromtimestamp(filedate)
591 today = date.today()
592 dmonth = (today.month-filedate.month) + 12*(today.year-filedate.year)
593 if dmonth>1 or (dmonth==1 and today.day>18):
594 docpi=True
595 except FileNotFoundError:
596 docpi=True
597
598if docpi:
599 headers = {'Content-type': 'application/json'}
600 yearlist = list(range(date.today().year,1912,-10))
601 if yearlist[-1]>1912:
602 yearlist.append(1912)
603 cpi=[]
604 lastcpi = 0
605 query = {"seriesid": ['CUUR0000SA0']}
606 if cpikey:
607 query["registrationkey"]=cpikey
608 ########################################################################
609 # The api.bls.gov site currently (2024-02-15) resolves to an
610 # IPv4 address which works, and an IPv6 address which does
611 # not. The urllib3 package does not currently implement the
612 # Happy Eyeballs algorithm, nor any other mechanism to re-try
613 # hung connections with alternative addresses returned by DNS.
614 # In the interest of expediency, we temporarily force the
615 # connection to api.bls.gov to only use IPv4; hopefully at
616 # some future date either urllib3 will gain the necessary
617 # features and/or the BLS will fix their configuration so
618 # that all A and AAAA records for api.bls.gov resolve to an
619 # operational server. At that time we can remove the three
620 # references to "requests.packages.urllib3.util.connection.HAS_IPV6"
621 # in this function.
622 ########################################################################
623 save_rpuucH = requests.packages.urllib3.util.connection.HAS_IPV6
624 requests.packages.urllib3.util.connection.HAS_IPV6 = False
625 for endyear in range(len(yearlist)-1):
626 query["startyear"]=str(yearlist[endyear+1]+1)
627 query["endyear"]=str(yearlist[endyear])
628 data = json.dumps(query)
629 p = requests.post('https://api.bls.gov/publicAPI/v2/timeseries/data/', data=data, headers=headers)
630 json_data = json.loads(p.text)
631 if json_data['status']=="REQUEST_NOT_PROCESSED":
632 docpi=False
633 stderr.write("Unable to update CPI data: Exceeded daily threshold for BLS requests\n")
634 break
635 for series in json_data['Results']['series']:
636 for item in series['data']:
637 if not ('M01' <= item['period'] <= 'M12'):
638 continue
639 year = int(item['year']) + int(item['period'][1:])/12
640 value = item['value']
641 cpi.append(' '*10 + "{} {} \\".format(year,value))
642 if lastcpi==0:
643 lastcpi=value
644 lastyear=year
645 firstcpi=value
646 firstyear=year
647 requests.packages.urllib3.util.connection.HAS_IPV6 = save_rpuucH
648if docpi: # Check again because request may have failed
649 cpi.reverse()
650 cpistr = '\n'.join(cpi)
651
652 cpi_text = ("""!message Consumer price index data from US BLS, {datestr}
653
654UScpi[1] noerror \\
655{cpistr}
656
657UScpi_now {lastcpi}
658UScpi_lastdate {lastyear}
659
660USdollars_in(date) units=[1;$] domain=[{firstyear},{lastyear}] \\
661 range=[1,{maxinfl}] \\
662 US$ UScpi_now / UScpi(date) ;\\
663 ~UScpi(US$ UScpi_now / USdollars_in)
664USinflation_since(date) units=[1;1] domain=[{firstyear},{lastyear}] \\
665 range=[1,{maxinfl}] \\
666 UScpi_now / UScpi(date) ;\\
667 ~UScpi(UScpi_now / USinflation_since)
668
669""".format(datestr=datestr,cpistr=cpistr,maxinfl=float(lastcpi)/float(firstcpi),lastcpi=lastcpi,
670 firstyear=firstyear,lastyear=lastyear)
671 ).replace('\n', linesep)
672
673#######################################################
674#
675# Format output and write the currency file
676#
677
678currency_text = (
679"""# ISO Currency Codes
680
681{codestr}
682
683# Currency exchange rates source
684
685!message Currency exchange rates from {sourcename} on {datestr}
686
687{ratestr}
688{bitcoinstr}
689
690# Precious metals prices from Packetizer (services.packetizer.com/spotprices)
691
692{metalstr}
693
694""".format(codestr=codestr, datestr=datestr, ratestr=ratestr, metalstr=metalstr,
695 bitcoinstr=bitcoinstr, sourcename=sourcename)
696).replace('\n', linesep)
697
698
699######################################
700
701try:
702 if currency_file == '-':
703 codecs.StreamReader(stdout, codecs.getreader('utf8')).write(currency_text)
704 else:
705 with codecs.open(currency_file, 'w', 'utf8') as of:
706 of.write(currency_text)
707except IOError as e:
708 stderr.write('Unable to write to output file:\n{}\n'.format(e))
709 exit(1)
710
711if docpi:
712 try:
713 if cpi_file == '-':
714 codecs.StreamReader(stdout, codecs.getreader('utf8')).write(cpi_text)
715 else:
716 with codecs.open(cpi_file, 'w', 'utf8') as of:
717 of.write(cpi_text)
718 except IOError as e:
719 stderr.write('Unable to write to output file:\n{}\n'.format(e))
720 exit(1)
721