File: cadex.py 1 #!/usr/bin/python3 2 3 # The MIT License (MIT) 4 # 5 # Copyright © 2024 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 # cadex [amount...] [currency code...] 27 # 28 # The CAnaDian EXchange is a currency converter for the canadian dollar (CAD) 29 # from/to all the major/top-traded international currencies. 30 # 31 # All data come straight from the Bank of Canada, and are updated at 4pm, 32 # Monday to Friday. 33 34 35 from datetime import datetime, timedelta 36 from json import loads 37 from sys import argv, exit, stderr, stdout 38 from urllib.request import urlopen 39 40 from typing import Dict, List 41 42 43 # info is the message shown when the leading argument is one of the standard 44 # cmd-line help options 45 info = ''' 46 cadex [amount...] [currency code...] 47 48 The CAnaDian EXchange is a currency converter for the canadian dollar (CAD) 49 from/to all the major/top-traded international currencies. 50 51 All data come straight from the Bank of Canada, and are updated at 4pm, 52 Monday to Friday. 53 '''.strip() 54 55 # a leading help-option arg means show the help message and quit 56 if len(argv) == 2 and argv[1].lower() in ('-h', '--h', '-help', '--help'): 57 print(info, file=stderr) 58 exit(0) 59 60 61 name_aliases: Dict[str, str] = { 62 'AU': 'AUD', 63 'BR': 'BRL', 64 'CH': 'CHF', 65 'CN': 'CNY', 66 'EU': 'EUR', 67 'EURO': 'EUR', 68 'GB': 'GBP', 69 'HK': 'HKD', 70 'ID': 'IDR', 71 'IN': 'INR', 72 'JP': 'JPY', 73 'KR': 'KRW', 74 'ME': 'MXN', 75 'MX': 'MXN', 76 'NO': 'NOK', 77 'NZ': 'NZD', 78 'PE': 'PEN', 79 'RU': 'RUB', 80 'SA': 'SAR', 81 'SE': 'SEK', 82 'SG': 'SGD', 83 'TR': 'TRY', 84 'TW': 'TWD', 85 'UK': 'GBP', 86 'US': 'USD', 87 'USA': 'USD', 88 'ZA': 'ZAR', 89 } 90 91 # which currencies to convert, and the order in which to present them in the output table 92 cur_list: List[str] = [ 93 'USD', 'MXN', 'EUR', 'GBP', 'AUD', 'CNY', 'INR', 'CHF', 'BRL', 'HKD', 'IDR', 94 'JPY', 'NZD', 'NOK', 'PEN', 'RUB', 'SAR', 'SGD', 'ZAR', 'KRW', 'SEK', 'TWD', 95 'TRY' 96 ] 97 98 cur_names: Dict[str, str] = { 99 'USD': 'US Dollar', 100 'MXN': 'Mexican Peso', 101 'EUR': 'Euro', 102 'GBP': 'British Pound', 103 'AUD': 'Australian Dollar', 104 'CNY': 'Chinese Yuan', 105 'INR': 'Indian Rupee', 106 'CHF': 'Swiss Franc', 107 'BRL': 'Brazilian Real', 108 'HKD': 'Hong Kong Dollar', 109 'IDR': 'Indonesian Rupiah', 110 'JPY': 'Japanese Yen', 111 'NZD': 'New Zealand Dollar', 112 'NOK': 'Norwegian Korona', 113 'PEN': 'Peruvian New Sol', 114 'RUB': 'Russian Ruble', 115 'SAR': 'Saudi Riyal', 116 'SGD': 'Singaporean Dollar', 117 'ZAR': 'South African Rand', 118 'KRW': 'Korean Wong', 119 'SEK': 'Swedish Korona', 120 'TWD': 'Taiwanese Dollar', 121 'TRY': 'Turkish Lira', 122 } 123 124 amount: float = 1 125 rates: Dict[str, float] = {} 126 currency: str = '' 127 128 for v in argv[1:]: 129 try: 130 amount = float(eval(v)) 131 except: 132 currency = v.upper() 133 if currency in name_aliases: 134 currency = name_aliases[currency] 135 136 stdout.reconfigure(newline='\n', encoding='utf-8') 137 138 # going back 3 days guarantees this script will work even on weekends, while 139 # minimizing data-transfers from the website 140 now = datetime.today() 141 since = now - timedelta(days=3) 142 143 # 144 # fetch and parse the exchange rates 145 # 146 147 # start with a reassuring message saying something is happening 148 ymd = f'{now.year}-{now.month:02}-{now.day:02}' 149 print(f'Using latest Bank of Canada data (from {ymd})', file=stderr) 150 stderr.flush() 151 152 boc = 'https://www.bankofcanada.ca' 153 base = boc + '/valet/observations/group/FX_RATES_DAILY/json' 154 uri = base + f'?start_date={since.year:02}-{since.month:02}-{since.day:02}' 155 156 with urlopen(uri) as c: 157 # exchange rates for the latest day observed: a dictionary with keys like 158 # FXAUDCAD, each of which leads to a dictionary where key 'v' gives the 159 # exchange rate as a string 160 data = loads(c.read())['observations'][-1] 161 rates = {} 162 for k in cur_list: 163 rates[k] = data[f'FX{k}CAD']['v'] 164 165 # empty line to let output breathe a bit 166 print(file=stderr) 167 stderr.flush() 168 169 header = ' from to rate name' 170 171 if currency != '' and currency in cur_list: 172 # only change between CAD and the currency given 173 x = amount 174 k = currency 175 v = float(rates[k]) 176 s = cur_names[k] 177 print(header, file=stderr) 178 stderr.flush() 179 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}') 180 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}') 181 exit(0) 182 183 # calculate and show all currency exchanges, converting in both directions 184 185 try: 186 # exchange amounts forward 187 print(header, file=stderr) 188 stderr.flush() 189 for i, k in enumerate(cur_list): 190 x = amount 191 v = float(rates[k]) 192 s = cur_names[k] 193 if i%10 < 5: 194 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}') 195 else: 196 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') 197 198 # empty line to let output breathe a bit 199 print(file=stderr) 200 stderr.flush() 201 202 # exchange amounts backward 203 print(header, file=stderr) 204 stderr.flush() 205 for i, k in enumerate(cur_list): 206 x = amount 207 v = float(rates[k]) 208 s = cur_names[k] 209 if i%10 < 5: 210 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}') 211 else: 212 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') 213 except BrokenPipeError: 214 # avoid showing broken-pipe messages from stdlib, in case not all of this 215 # script's output was needed 216 stderr.flush() 217 stderr.close()