rayhamel
9/18/2017 - 8:19 PM

units_cur4.py

#!/usr/bin/python
#
# Version 2  
# 9 July 2013
#
# Modified to use xml format output from timegenie instead of text file
#
# Version 3
# 7 March 2014
#
# Added checks for unicode currency names
#
# Version 3.1
# 23 June 2014
#
# Added test for non-unicode strings to prevent a runtime warning
#
# Version 4
# 15 September 2017 (Ray Hamel)
#
# Rewritten to use Yahoo YQL API due to removal of TimeGenie RSS feed
# Now makes requests over HTTPS
# Now uses JSON version of the Packetizer spotprices API
# Added Bitcoin and Ethereum
#

# For Python 2 & 3 compatibility
from __future__ import absolute_import, division, print_function
try:
    from urllib.parse import quote
    from urllib.request import urlopen
except ImportError:
    from urllib import quote
    from urllib2 import urlopen

# Normal imports
import codecs
import json
from argparse import ArgumentParser, FileType
from collections import OrderedDict
from datetime import date
from decimal import Decimal
from os import linesep
from sys import exit, stderr, stdout

# The Yahoo API is excellent, but unfortunately requires we explicitly request
# each currency we want
#
# EU members that may join the Eurozone in the future, and so may need to be
# removed from this list
#
# ('BGN', 'bulgarialev'),
# ('CZK', 'czechkoruna'),
# ('DKK', 'denmarkkrona'),
# ('HRK', 'croatiakuna'),
# ('HUF', 'hungariaforint'),
# ('PLN', 'polandzloty'),
# ('RON', 'romanianewlei'),
# ('SEK', 'swedenkrona'),
currency_list = OrderedDict([
    ('USD', 'usdollar'), # Yahoo API returns fractions of USD, so always 1.0
    ('AED', 'uaedirham'),
    ('AFN', 'afghanafghani'),
    ('ALL', 'albanialek'),
    ('AMD', 'armeniadram'),
    ('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'),
    ('BYR', 'belarusruble'),
    ('BZD', 'belizedollar'),
    ('CAD', 'canadadollar'),
    ('CDF', 'drcfranccongolais'),
    ('CHF', 'swissfranc'),
    ('CLP', 'chilepeso'),
    # ('CMG', 'sintmaartencaribbeanguilder'), # Not supported by Yahoo API
    ('CNY', 'chinayuan'),
    ('COP', 'colombiapeso'),
    ('CRC', 'costaricacolon'),
    ('CUP', 'cubapeso'),
    ('CVE', 'capeverdeescudo'),
    ('CZK', 'czechkoruna'),
    ('DJF', 'djiboutifranc'),
    ('DKK', 'denmarkkrona'),
    ('DOP', 'dominicanrepublicpeso'),
    ('DZD', 'algeriadinar'),
    ('EGP', 'egyptpound'),
    ('ERN', 'eritreanakfa'),
    ('ETB', 'ethiopianbirr'),
    ('EUR', 'euro'),
    ('FJD', 'fijidollar'),
    ('FKP', 'falklandislandspound'),
    ('GBP', 'ukpound'),
    ('GEL', 'georgialari'),
    # ('GGP', 'guernseypound'), # Not supported by Yahoo API
    ('GHS', 'ghanacedi'),
    ('GIP', 'gibraltarpound'),
    ('GMD', 'gambiadalasi'),
    ('GNF', 'guineafranc'),
    ('GTQ', 'guatemalaquetzal'),
    ('GYD', 'guyanadollar'),
    ('HKD', 'hongkongdollar'),
    ('HNL', 'honduraslempira'),
    ('HRK', 'croatiakuna'),
    ('HTG', 'haitigourde'),
    ('HUF', 'hungariaforint'),
    ('IDR', 'indonesiarupiah'),
    ('ILS', 'israelnewshekel'),
    # ('IMP', 'manxpound'), # Not supported by Yahoo API
    ('INR', 'indiarupee'),
    ('IQD', 'iraqdinar'),
    ('IRR', 'iranrial'),
    ('ISK', 'icelandkrona'),
    # ('JEP', 'jerseypound'), # Not supported by Yahoo API
    ('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', 'srilankanrupee'),
    ('LRD', 'liberiadollar'),
    ('LTL', 'lithuanialita'),
    ('LVL', 'latvialat'),
    ('LYD', 'libyadinar'),
    ('MAD', 'moroccodirham'),
    ('MDL', 'moldovaleu'),
    ('MGA', 'madagascarariary'),
    ('MKD', 'macedoniadenar'),
    ('MMK', 'myanmarkyat'),
    ('MNT', 'mongoliatugrik'),
    ('MOP', 'macaupataca'),
    ('MRO', 'mauritaniaouguiya'),
    ('MUR', 'mauritiusrupee'),
    ('MVR', 'maldiverufiyaa'),
    ('MWK', 'malawikwacha'),
    ('MXN', 'mexicopeso'),
    ('MYR', 'malaysiaringgit'),
    ('MZN', 'mozambicanmetical'),
    ('NAD', 'namibiadollar'),
    ('NGN', 'nigerianaira'),
    ('NIO', 'nicaraguacordobaoro'),
    ('NOK', 'norwaykrone'),
    ('NPR', 'nepalrupee'),
    ('NZD', 'newzealanddollar'),
    ('OMR', 'omanrial'),
    ('PAB', 'panamabalboa'),
    ('PEN', 'perunuevosol'),
    ('PGK', 'papuanewguineakina'),
    ('PHP', 'philippinepeso'),
    ('PLN', 'polandzloty'),
    ('PKR', 'pakistanrupee'),
    ('PYG', 'paraguayguarani'),
    ('QAR', 'qatarrial'),
    ('RON', 'romanianewlei'),
    ('RSD', 'serbiadinar'),
    ('RUB', 'russiarouble'),
    ('RWF', 'rwandafranc'),
    ('SAR', 'saudiarabiariyal'),
    ('SBD', 'solomonislandsdollar'),
    ('SCR', 'seychellesrupee'),
    ('SDG', 'sudanpound'),
    ('SEK', 'swedenkrona'),
    ('SGD', 'singaporedollar'),
    ('SHP', 'sainthelenapound'),
    ('SLL', 'sierraleoneleone'),
    ('SOS', 'somaliaschilling'),
    ('SRD', 'surinamedollar'),
    ('STD', 'saotome&principedobra'),
    ('SVC', 'elsalvadorcolon'),
    ('SYP', 'syriapound'),
    ('SZL', 'swazilandlilangeni'),
    ('THB', 'thailandbaht'),
    ('TJS', 'tajikistansomoni'),
    ('TMT', 'turkmenistanmanat'),
    ('TND', 'tunisiadinar'),
    ('TOP', "tongapa'anga"),
    ('TRY', 'turkeylira'),
    ('TTD', 'trinidadandtobagodollar'),
    # ('TVD', 'tuvaludollar'), # Not supported by Yahoo API
    ('TWD', 'taiwandollar'),
    ('TZS', 'tanzaniashilling'),
    ('UAH', 'ukrainehryvnia'),
    ('UGX', 'ugandaschilling'),
    ('UYU', 'uruguaypeso'),
    ('UZS', 'uzbekistansum'),
    ('VEF', 'venezuelabolivar'),
    ('VND', 'vietnamdong'),
    ('VUV', 'vanuatuvatu'),
    ('WST', 'samoatala'),
    ('XAF', 'centralafricancfafranc'),
    ('XCD', 'eastcaribbeandollar'),
    ('XOF', 'westafricanfranc'),
    ('XPF', 'cfpfranc'),
    ('YER', 'yemenrial'),
    ('ZAR', 'southafricarand'),
    ('ZMW', 'zambiakwacha'),
    ('ZWL', 'zimbabwedollar'),
    # _IMF Special Drawing Rights_
    ('XDR', 'specialdrawingrights'),
    # _Cryptocurrencies_
    ('BTC', 'bitcoin'),
    ('ETH', 'ethereum'),
    # _Precious metals_
    # ('XAG', 'silverprice'), # Yahoo API is less accurate than Packetizer
    # ('XAU', 'goldprice'), # Yahoo API is less accurate than Packetizer
    # ('XPD', 'palladiumprice'), # Yahoo API is less accurate than Packetizer
    # ('XPT', 'platinumprice'), # Yahoo API is less accurate than Packetizer
    ])

outfile_name = 'currency.units'

ap = ArgumentParser(
    description="Update currency information for 'units' "
    "into the specified filename or if no filename is "
    "given, the default: '{}'".format(outfile_name),
    )

ap.add_argument(
    'output_file',
    default=outfile_name,
    help='the file to update',
    metavar='filename',
    nargs='?',
    type=str,
    )

outfile_name = ap.parse_args().output_file

# Can't use 'urlencode' here because it replaces ' ' with '+'
params = '&'.join((
    'env=store://datatables.org/alltableswithkeys',
    'format=json',
    'q=' + quote('SELECT Rate FROM yahoo.finance.xchange '
                 # Request USD first and last to check these assumptions
                 # remain true: the Yahoo API returns exchange rates in
                 # the order they're requested, and in USD terms
                 'WHERE pair IN ("{}","USD")'.format(
                    '","'.join(currency_list.keys()),
                    )),
    ))

try:
    rates = [c['Rate'] for c in json.load(
        # Can't use the 'data=' parameter here because we're making a GET
        # request, not a POST
        urlopen('https://query.yahooapis.com/v1/public/yql?{}'.format(params)),
        )['query']['results']['rate']]
except IOError as e:
    stderr.write('Error connecting to currency server:\n{}\n'.format(e))
    exit(1)

assert all(Decimal(r) == 1 for r in (rates[0], rates[-1])), (
    "Yahoo API didn't return exchange rates in the order they were requested, "
    "or didn't return them in USD terms"
    )

del rates[-1] # Extra USD

assert len(currency_list) == len(rates), (
    "Yahoo API returned fewer results than the number of currencies requested"
    )

try:
    metals = json.load(
        urlopen('https://services.packetizer.com/spotprices/?f=json'),
        )
except IOError as e:
    stderr.write('Error connecting to spotprices server:\n{}\n'.format(e))
    exit(1)

del metals['date']

class Currency(object):
    """
    Contains the information necessary to list a currency or similar
    (cryptocurrency, precious metal) in a 'currency.units' file

    'from_usd' is the amount 1 USD buys (what the Yahoo API returns)
    'to_usd' is the amount of USD 1 unit of this currency buys
    """
    def __init__(self, iso_code, unit, from_usd):
        self.iso_code = iso_code
        self.unit = unit
        self.to_usd = (1 / Decimal(from_usd)).quantize(
            Decimal('.0001'), # Yahoo API returns results to 4 decimal places
            )

currencies = []

for (iso_code, unit), from_usd in zip(currency_list.items(), rates):
    if from_usd == 'N/A':
        stderr.write('no rate for "{}" ({})'.format(unit, iso_code))
        continue
    currencies.append(Currency(iso_code, unit, from_usd))

codestr = '\n'.join('{:23}{}'.format(c.iso_code, c.unit) for c in currencies)

datestr = date.today().isoformat()

maxlen = max(len(c.unit) for c in currencies) + 2

del currencies[0] # USD, usdollar, 1.0000

ratestr = '\n'.join(
    '1|{:{}}{} US$'.format(c.unit, maxlen, c.to_usd) for c in currencies
    )

ozzystr = '\n'.join('{:19}{} US$/troyounce'.format(
    metal + 'price',
    price,
    ) for metal, price in metals.items())

outstr = (
"""ISO Currency Codes

{codestr}

# Currency exchange rates from Yahoo Finance (finance.yahoo.com)

!message Currency exchange rates from finance.yahoo.com on {datestr}

{ratestr}

# Precious metals prices from Packetizer (services.packetizer.com/spotprices)

{ozzystr}
""".format(codestr=codestr, datestr=datestr, ratestr=ratestr, ozzystr=ozzystr)
).replace('\n', linesep)

try:
    if outfile_name == '-':
        codecs.StreamReader(stdout, codecs.getreader('utf8')).write(outstr)
    else:    
        with codecs.open(outfile_name, 'w', 'utf8') as of:
            of.write(outstr)
except IOError as e:
    stderr.write('Unable to write to output file:\n{}\n'.format(e))
    exit(1)