File: cadex.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2020-2025 pacman64 6 # 7 # Permission is hereby granted, free of charge, to any person obtaining a copy 8 # of this software and associated documentation files (the “Software”), to deal 9 # in the Software without restriction, including without limitation the rights 10 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 # copies of the Software, and to permit persons to whom the Software is 12 # furnished to do so, subject to the following conditions: 13 # 14 # The above copyright notice and this permission notice shall be included in 15 # all copies or substantial portions of the Software. 16 # 17 # THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 # SOFTWARE. 24 25 26 from datetime import datetime, timedelta 27 from json import loads 28 from sys import argv, exit, stderr 29 from typing import Dict, List 30 from urllib.request import urlopen 31 32 33 info = ''' 34 cadex [amount...] [currency code...] 35 36 The CAnaDian EXchange is a currency converter for the canadian dollar (CAD) 37 from/to all the major/top-traded international currencies. 38 39 All data come straight from the Bank of Canada, and are updated at 4pm, 40 Monday to Friday. 41 ''' 42 43 # a leading help-option arg means show the help message and quit 44 if len(argv) == 2 and argv[1] in ('-h', '--h', '-help', '--help'): 45 print(info.strip()) 46 exit(0) 47 48 49 name_aliases: Dict[str, str] = { 50 'AU': 'AUD', 51 'BR': 'BRL', 52 'CH': 'CHF', 53 'CN': 'CNY', 54 'EU': 'EUR', 55 'EURO': 'EUR', 56 'GB': 'GBP', 57 'HK': 'HKD', 58 'ID': 'IDR', 59 'IN': 'INR', 60 'JP': 'JPY', 61 'KR': 'KRW', 62 'ME': 'MXN', 63 'MX': 'MXN', 64 'NO': 'NOK', 65 'NZ': 'NZD', 66 'PE': 'PEN', 67 'RU': 'RUB', 68 'SA': 'SAR', 69 'SE': 'SEK', 70 'SG': 'SGD', 71 'TR': 'TRY', 72 'TW': 'TWD', 73 'UK': 'GBP', 74 'US': 'USD', 75 'USA': 'USD', 76 'ZA': 'ZAR', 77 } 78 79 # which currencies to convert, and the order in which to present them in the output table 80 cur_list: List[str] = [ 81 'USD', 'MXN', 'EUR', 'GBP', 'AUD', 'CNY', 'INR', 'CHF', 'BRL', 'HKD', 'IDR', 82 'JPY', 'NZD', 'NOK', 'PEN', 'RUB', 'SAR', 'SGD', 'ZAR', 'KRW', 'SEK', 'TWD', 83 'TRY' 84 ] 85 86 cur_names: Dict[str, str] = { 87 'USD': 'US Dollar', 88 'MXN': 'Mexican Peso', 89 'EUR': 'Euro', 90 'GBP': 'British Pound', 91 'AUD': 'Australian Dollar', 92 'CNY': 'Chinese Yuan', 93 'INR': 'Indian Rupee', 94 'CHF': 'Swiss Franc', 95 'BRL': 'Brazilian Real', 96 'HKD': 'Hong Kong Dollar', 97 'IDR': 'Indonesian Rupiah', 98 'JPY': 'Japanese Yen', 99 'NZD': 'New Zealand Dollar', 100 'NOK': 'Norwegian Korona', 101 'PEN': 'Peruvian New Sol', 102 'RUB': 'Russian Ruble', 103 'SAR': 'Saudi Riyal', 104 'SGD': 'Singaporean Dollar', 105 'ZAR': 'South African Rand', 106 'KRW': 'Korean Wong', 107 'SEK': 'Swedish Korona', 108 'TWD': 'Taiwanese Dollar', 109 'TRY': 'Turkish Lira', 110 } 111 112 amount: float = 1 113 rates: Dict[str, float] = {} 114 currency: str = '' 115 116 for v in argv[1:]: 117 try: 118 amount = float(eval(v)) 119 except Exception: 120 currency = v.upper() 121 if currency in name_aliases: 122 currency = name_aliases[currency] 123 124 # going back 3 days guarantees this script will work even on weekends, while 125 # minimizing data-transfers from the website 126 now = datetime.today() 127 since = now - timedelta(days=3) 128 129 # 130 # fetch and parse the exchange rates 131 # 132 133 # start with a reassuring message saying something is happening 134 ymd = f'{now.year}-{now.month:02}-{now.day:02}' 135 print(f'Using latest Bank of Canada data (from {ymd})', file=stderr) 136 137 boc = 'https://www.bankofcanada.ca' 138 base = boc + '/valet/observations/group/FX_RATES_DAILY/json' 139 uri = base + f'?start_date={since.year:02}-{since.month:02}-{since.day:02}' 140 141 with urlopen(uri) as c: 142 # exchange rates for the latest day observed: a dictionary with keys like 143 # FXAUDCAD, each of which leads to a dictionary where key 'v' gives the 144 # exchange rate as a string 145 data = loads(c.read())['observations'][-1] 146 rates = {} 147 for k in cur_list: 148 rates[k] = data[f'FX{k}CAD']['v'] 149 150 # empty line to let output breathe a bit 151 print(file=stderr) 152 153 header = ' from to rate name' 154 155 if currency != '' and currency in cur_list: 156 # only change between CAD and the currency given 157 x = amount 158 k = currency 159 v = float(rates[k]) 160 s = cur_names[k] 161 print(header, file=stderr) 162 print(f'\x1b[38;5;196m{x:15,.2f}\x1b[0m CAD = {x/v:15,.2f} {k} \x1b[38;5;208m{v:15,.4f}\x1b[0m {s}') 163 print(f'{x:15,.2f} {k} = \x1b[38;5;196m{x*v:15,.2f}\x1b[0m CAD \x1b[38;5;208m{v:15,.4f}\x1b[0m {s}') 164 exit(0) 165 166 # calculate and show all currency exchanges, converting in both directions 167 168 try: 169 # exchange amounts forward 170 print(header, file=stderr) 171 for i, k in enumerate(cur_list): 172 x = amount 173 v = float(rates[k]) 174 s = cur_names[k] 175 if i%10 < 5: 176 print(f'\x1b[38;5;196m{x:15,.2f}\x1b[0m CAD = {x/v:15,.2f} {k} \x1b[38;5;208m{v:15,.4f}\x1b[0m {s}') 177 else: 178 print(f'\x1b[38;5;196m{x:15,.2f}\x1b[0m CAD = \x1b[38;5;244m{x/v:15,.2f} {k}\x1b[0m \x1b[38;5;208m{v:15,.4f}\x1b[0m \x1b[38;5;244m{s}\x1b[0m') 179 180 # empty line to let output breathe a bit 181 print(file=stderr) 182 183 # exchange amounts backward 184 # print(header, file=stderr) 185 for i, k in enumerate(cur_list): 186 x = amount 187 v = float(rates[k]) 188 s = cur_names[k] 189 if i%10 < 5: 190 print(f'{x:15,.2f} {k} = \x1b[38;5;196m{x*v:15,.2f}\x1b[0m CAD \x1b[38;5;208m{v:15,.4f}\x1b[0m {s}') 191 else: 192 print(f'\x1b[38;5;244m{x:15,.2f} {k} =\x1b[0m \x1b[38;5;196m{x*v:15,.2f}\x1b[0m CAD \x1b[38;5;208m{v:15,.4f}\x1b[0m \x1b[38;5;244m{s}\x1b[0m') 193 except BrokenPipeError: 194 # avoid showing broken-pipe messages from stdlib, in case not all of this 195 # script's output was needed 196 stderr.close()