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()