File: guicadex.pyw 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 # guicadex.pyw 27 # 28 # The GUI version of the CAnadian EXchange, a multi-currency converter around 29 # the canadian dollar. 30 31 32 import math 33 from math import \ 34 acos, acosh, asin, asinh, atan, atan2, atanh, ceil, comb, \ 35 copysign, cos, cosh, degrees, dist, e, erf, erfc, exp, expm1, \ 36 fabs, factorial, floor, fmod, frexp, fsum, gamma, gcd, hypot, inf, \ 37 isclose, isfinite, isinf, isnan, isqrt, lcm, ldexp, lgamma, log, \ 38 log10, log1p, log2, modf, nan, nextafter, perm, pi, pow, prod, \ 39 radians, remainder, sin, sinh, sqrt, tan, tanh, tau, trunc, ulp 40 try: 41 from math import cbrt, exp2 42 except: 43 pass 44 45 from random import \ 46 betavariate, choice, choices, expovariate, gammavariate, gauss, \ 47 getrandbits, getstate, lognormvariate, normalvariate, paretovariate, \ 48 randbytes, randint, random, randrange, sample, seed, setstate, \ 49 shuffle, triangular, uniform, vonmisesvariate, weibullvariate 50 51 from statistics import \ 52 bisect_left, bisect_right, fmean, \ 53 geometric_mean, harmonic_mean, mean, median, \ 54 median_grouped, median_high, median_low, mode, multimode, pstdev, \ 55 pvariance, quantiles, stdev, variance 56 try: 57 from statistics import \ 58 correlation, covariance, linear_regression, mul, reduce 59 except: 60 pass 61 62 from json import loads 63 from urllib.request import urlopen 64 from datetime import datetime, timedelta 65 66 from typing import Dict 67 from tkinter.ttk import Treeview 68 from tkinter.messagebox import showerror 69 from tkinter import Tk, Label, Button, Entry, Frame, END, StringVar 70 71 72 amount: float = 1 73 rates: Dict[str, float] = {} 74 75 # cur_list is the list of currencies to convert, in the order in which to 76 # present them in the output table 77 cur_list = [ 78 'USD', 'MXN', 'EUR', 'GBP', 'AUD', 'CNY', 'INR', 'CHF', 'BRL', 'HKD', 79 'IDR', 'JPY', 'NZD', 'NOK', 'PEN', 'RUB', 'SAR', 'SGD', 'ZAR', 'KRW', 80 'SEK', 'TWD', 'TRY' 81 ] 82 83 # quick guide for the treeview-specific api 84 # https://riptutorial.com/tkinter/example/31880/treeview--basic-example 85 86 87 def update() -> None: 88 '''Evaluates the current value/formula, then updates the bottom table''' 89 90 global amount 91 try: 92 amount = eval(inp.get()) 93 inp.config(bg='white', fg='black') 94 except: 95 amount = 1 96 inp.config(bg='darkred', fg='white') 97 # clear the output 98 for c in out.get_children(): 99 out.delete(c) 100 101 # update output rows 102 out.heading('#0', text=f'{amount:,.2f}') 103 out.heading('from CAD', text=f'{amount:,.2f} CAD is') 104 j = 0 # the actual row-insertion index 105 for i, (k, v) in enumerate(rates.items()): 106 # visually-separate groups of 3 rows by adding an empty one 107 if i > 0 and i % 3 == 0: 108 out.insert('', j, text='') 109 j += 1 110 # convert to and from canadian dollars using the current currency 111 val = [f'{float(v)*amount:,.2f} CAD', f'{amount/float(v):,.2f} {k}'] 112 out.insert('', j, text=k, values=val) 113 j += 1 114 115 116 def get_rates() -> None: 117 '''Fetches/parses the exchange rates, then causes a GUI update''' 118 119 boc = 'https://www.bankofcanada.ca' 120 base = boc + '/valet/observations/group/FX_RATES_DAILY/json?start_date=' 121 now = datetime.today() 122 since = now - timedelta(days=7) 123 uri = base + f'{since.year:02}-{since.month:02}-{since.day:02}' 124 125 with urlopen(uri) as c: 126 global rates 127 # exchange rates for the latest day observed: a dictionary with keys 128 # like FXAUDCAD, each of which leads to a dictionary where key 'v' 129 # gives the exchange rate as a string 130 data = loads(c.read())['observations'][-1] 131 rates = {} 132 for k in cur_list: 133 rates[k] = data[f'FX{k}CAD']['v'] 134 135 update() 136 137 138 try: 139 win = Tk() 140 win.title('Canadian Exchange') 141 win.bind('<Escape>', lambda _: win.quit()) 142 143 # top of window: amount to convert and button to update exchange rates 144 top = Frame(win) 145 top.pack() 146 Label(top, text='Amount', font=20).grid(row=0, column=0, padx=5) 147 inp = Entry(top, text=StringVar(value=1), width=18, borderwidth=1) 148 inp.select_range(0, END) 149 inp.grid(row=0, column=1, padx=0) 150 inp.bind('<KeyRelease>', lambda _: update()) 151 inp.focus_set() 152 rb = Button(top, text='Update Exchange Rates', font=14, command=get_rates) 153 rb.grid(row=0, column=2, padx=0, sticky='e') 154 155 # bottom of window only has the conversions table 156 bottom = Frame(win) 157 bottom.pack(padx=0) 158 out = Treeview(bottom, column=['to CAD', 'from CAD'], height=30) 159 out.heading('#0', text='currency') 160 out.column('#0', width=100) 161 out.heading('to CAD', text='to CAD') 162 out.column('to CAD', width=150, anchor='e') 163 out.heading('from CAD', text='from CAD') 164 out.column('from CAD', width=160, anchor='e') 165 out.pack() 166 167 # fetch exchange rates, and only then run the GUI 168 get_rates() 169 win.mainloop() 170 except Exception as e: 171 showerror('Error', str(e)) 172 win.quit()