Saturday, October 29, 2022

Python - Project - Flash Card (Day 31)

This is a 100 Days challenge to learn a new language (Python). 100 Days of Code - The Complete Python Pro Bootcamp 

I will post some notes to motivate myself to finish this challenge.


Goal



1. Using Tkinter to build a GUI
a. Using canvas to mix images and texts
b. Binding functions to buttons
c. Utilizing grid layout for the better look
d. Lean how to update Tkinter elements

2. Deal with csv file through pandas
a. Support reading and writting
b. Studying some parameters when transform DataFrame to other DataType

DataFrame.to_dict(orient="records")

3. Using after and after_cancel to manage events

4. Try to use Error Handling as we learned on Day 30


Requirements



1. Build a flash card app which read data from csv file.

2. After 3 seconds, app should flip the card to see the translated word.

3. Next word should be shown after both buttons got clicked.

4. Once users click 'mark' button (or I knew button), then we should not let users see this word anymore.

5. Save the progress to csv file and read data from it from the next play.


Project - Flash Card





Ex:
import tkinter as tk
import pandas as pd
import random
import os

BACKGROUND_COLOR = "#B1DDC6"
FONT = "Ariel"
ALL_WORDS_CSV_FILE = "data/1000 words (en-to-zh-tw).csv"
WORDS_TO_LEARN_CSV_FILE = "data/words_to_learn.csv"

# Variables
words_dic = {}
selected_word = {}
filp_timer = ""

# Read a comma-separated values (csv) file into DataFrame.
# NOTE:
# Using Exception handling here for practice only becuse it is easy
# to be abused
# It is better to find a proper way
# (such as adding some if-else condition)
try:
    words_to_learn_data_frame = pd.read_csv(WORDS_TO_LEARN_CSV_FILE)
except FileNotFoundError:
    # If we cannot find the historical data,
     # then we read data from raw csv
    all_words_data_frame = pd.read_csv(ALL_WORDS_CSV_FILE)
    words_dic = all_words_data_frame.to_dict(orient="records")
else:
    words_dic = words_to_learn_data_frame.to_dict(orient="records")


def game_over():
    """Show game over message, and remvoe the historical data"""

    # Clear title and word text
    canvas.delete(title_text)
    canvas.delete(word_text)

    # Add game over message
    canvas.create_text(
        400,
        170,
        text="You have learned all words",
        fill="black",
        font=(FONT, 40, "bold"),
    )

    # Remove 'words_to_learn.csv'
    if os.path.exists(WORDS_TO_LEARN_CSV_FILE):
        os.remove(WORDS_TO_LEARN_CSV_FILE)


def right_btn_clicked():
    """Need to remove the current word from the list and
dump the updated list to csv file, then generate the next word"""

    # Remove the previous word since users knew it
    global selected_word
    if selected_word:
        words_dic.remove(selected_word)
        # Reset it
        selected_word = {}

    # Export current words collection to csv
    words_to_learn_data_frame = pd.DataFrame.from_dict(words_dic)
    words_to_learn_data_frame.to_csv(
"data/words_to_learn.csv",
index=False
)

    next_word()


def next_word():
    """Randomly select a word and show it in card"""

    # cancel previous timer if needed
    global filp_timer
    if not filp_timer == "":
        root.after_cancel(filp_timer)

    # Game Over - No more words to learn
    if len(words_dic) == 0:
        game_over()

        return

    # Change the canvas image to front_image
    canvas.itemconfig(canvas_image, image=front_image)

    # Randomly select a word and save it to the
# global variable selected_word
    global selected_word
    selected_word = random.choice(words_dic)

    # Change title and word
    canvas.itemconfig(title_text, fill="black", text="English")
    canvas.itemconfig(word_text, fill="black",
text=selected_word.get("English"))

    # Update global variable filter_timer
    filp_timer = root.after(3000, flip_card)


def flip_card():
    """Show Chinese Word"""

    # Change the canvas image to back_image
    canvas.itemconfig(canvas_image, image=back_image)

    # Change title and word
    canvas.itemconfig(title_text, fill="white", text="Chinese")
    canvas.itemconfig(
        word_text, fill="white",
text=selected_word.get("Traditional Chinese")
    )


# Init Tkinter
root = tk.Tk()
root.title("Flash Card")
root.config(padx=50, pady=50, background=BACKGROUND_COLOR)

# Init canvas
canvas = tk.Canvas(
    width=800, height=526, background=BACKGROUND_COLOR,
highlightthickness=0
)

# Add card_front image and set it to the center of the canvas
front_image = tk.PhotoImage(file="images/card_front.png")
back_image = tk.PhotoImage(file="images/card_back.png")
canvas_image = canvas.create_image(400, 263, image=front_image)

# Add Title Text, and set it to the middle and a bit up of the canvas
title_text = canvas.create_text(
    400, 150, text="", fill="black", font=(FONT, 40, "italic")
)
# Add Word Text, and set it to the middle and a bit down of the canvas
word_text = canvas.create_text(400, 253, text="", fill="black",
font=(FONT, 60, "bold"))

canvas.grid(column=0, row=0, columnspan=2)

# Wrong Btn
wrong_image = tk.PhotoImage(file="images/wrong.png")
wrong_btn = tk.Button(image=wrong_image, command=next_word)
wrong_btn.config(background=BACKGROUND_COLOR,
activebackground=BACKGROUND_COLOR)
wrong_btn.grid(sticky="ew", column=0, row=1)

# Right Btn
right_image = tk.PhotoImage(file="images/right.png")
right_btn = tk.Button(image=right_image, command=right_btn_clicked)
right_btn.config(background=BACKGROUND_COLOR,
activebackground=BACKGROUND_COLOR)
right_btn.grid(sticky="ew", column=1, row=1)

# Get the first Word Card
next_word()

# Start the Event Loop
root.mainloop()

Sunday, October 9, 2022

Python - Exceptions and JSON (Day 30)

This is a 100 Days challenge to learn a new language (Python). 100 Days of Code - The Complete Python Pro Bootcamp 

I will post some notes to motivate myself to finish this challenge.


Exceptions



When exceptions occur, python program will stop and show some error messages.

There are some normal exceptions below.


FileNotFound



Ex: 

with open("no_such_file.txt") as file:
    print(file.read())

Result:
FileNotFoundError:
[Errno 2] No such file or directory: 'no_such_file.txt'


KeyError



Ex: 
my_dict = {"key1": "value1", "key2": "value2"}
print(my_dict["no_such_key"])

Result:
KeyError: 'no_such_key'


IndexError



Ex: 
my_list = [1, 2, 3]
print(my_list[4])

Result:
IndexError: list index out of range


TypeError



Ex: 
my_str = "hello"
print(my_str + 123)

Result:
TypeError: can only concatenate str (not "int") to str


How to catch Exceptions



Using the following keywords:

        try:
            Something that might cause an exception.

        except:
            Do this if there was an exception.

        else:
            Do this if there were no exceptions.

        finally:
            Do this no matter what happens.


Ex: 
try:
    file = open("no_such_file.txt")
except:
    print("except")
    file = open("no_such_file.txt", "w")
    file.write("new created file")
else:
    print("else")
    print(file.read())
finally:
    print("finally")
    file.close()

Result (first run):
except
finally

Result (second run):
else
new created file
finally


Our own exceptions?



We can utilize 'raise' statement and some built-in exception to create our own exceptions.

Take the following exp as an example, there is no syntax errors or exceptions if users enter their height larger than 300. But it does not make sense to be an input parameter as human's height.

Ex:

height = float(input("Your height in cm: "))
weight = float(input("Your weight in kg: "))

# Normal Human Being cannot be higher than 300cm
if height > 300:
    raise ValueError("Height should not be larger than 300")

bmi = weight / (height / 100) ** 2

print(f"Your bmi is {weight / (height / 100) ** 2}")


Challenging - Revise NATO Alphabet Project



Catch exceptions if users enter non-alphabet characters.


Ex:
import pandas

# Using pandas to read csv file
nato_phonetic_alphabet_data_frame =
pandas.read_csv("nato_phonetic_alphabet.csv")

# Generate a dictionary to use alphabet as a key and relating word
as a value
# {"A": "Alfa", "B": "Bravo"}
nato_phonetic_alphabet_dict = {
    row["letter"]: row["code"]
    for (index, row) in nato_phonetic_alphabet_data_frame.iterrows()
}


""" Generate phonetic alphabet result based on the user input """
def generate_phonetic_alphabet_result():

    # Ask users to enter a word
    user_input = input("Enter a word? ")

    try:
        # Use List Comprehension to create a result list
        # Loop through all letters of user input and get
the mapping word by dictionary
        result = [nato_phonetic_alphabet_dict[letter] for letter in
user_input.upper()]
    except KeyError:
        # If users enter non-alphabet characters, then we cannot find it
from the nato_phonetic_alphabet_dict dictionary
        # And program will throw KeyError Exception
        print("Sorry, only letters in the alphabet please")

        # In this case, we need to call the self function again
        generate_phonetic_alphabet_result()
    else:
        print(result)


# Call this function to ask user input and then get relating results
generate_phonetic_alphabet_result()



JSON



JSON, JavaScript Object Notation.

Format: (look like python dictionary)
  {
      key: value,
      key: value,
  }

Ex (Write):

import json

# Write
with open("data.json", "w") as file:
    json.dump({"key1": "value1"}, file, indent=4)


Result:
// data.json
  {
      "key1": "value1"
  }

Ex (Read):

import json

# Load
with open("data.json", "r") as file:
    print(json.load(file))

Result:
{'key1': 'value1'}

Ex (Update):

import json

# Update
# Read it frist
with open("data.json", "r") as file:
    # Load JSON file first
    data = json.load(file)
    # Use update to add more data
    data.update(
        {
            "key2": {
                "inner_key1": "innver_value1",
                "inner_key2": "innver_value2",
            }
        }
    )
# Write it back
with open("data.json", "w") as file:
    # Use dump function to write the updated data back to the file
    json.dump(data, file, indent=4)

Result:
// data.json
  {
     "key1": "value1",
          "key2": {
              "inner_key1": "innver_value1",
              "inner_key2": "innver_value2"
      }
  }


Challenging - Revise Password Manager





#1 Save file from txt to json.
#2 Catch Exceptions
#3 Add Search function

Ex:
import random
import tkinter as tk
from tkinter import messagebox
import pyperclip
import json


def find_password():
    """Find password from data.json"""
    website = website_entry.get()

    # Validation
    if len(website) == 0:
        messagebox.showerror(
            title="Info",
message="Please make sure to enter website field"
        )

        return

    try:
        # Load data from data.json
        with open("data.json", encoding="utf-8", mode="r") as file:
            data = json.load(file)
    except FileNotFoundError:
        messagebox.showerror(title="Info", message="No Data File Found")
    else:
        # Continue from try clause
        # If we can find website from data.json
        if website in data:
            websiteObj = data[website]
            messagebox.showinfo(
                title=website,
                message=f"Email:{websiteObj['email']}\nPassword:
{websiteObj['pwd']}",
            )
        else:
            messagebox.showerror(
                title="Info",
message=f"No details for the {website} exists"
            )


def copy_to_clipboard(message):
    """Use pyperclip library to copy message to clipboard"""
    pyperclip.copy(message)


def generate_password():
    """Generate random password and show it to GUI"""
    letters = [
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
        "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
    ]
    numbers = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
    symbols = ["!", "#", "$", "%", "&", "(", ")", "*", "+"]

    password_letter_list = [
        random.choice(letters) for _ in range(random.randint(8, 10))
    ]
    password_symbols_list = [
        random.choice(symbols) for _ in range(random.randint(2, 4))
    ]
    password_numbers_list = [
        random.choice(numbers) for _ in range(random.randint(2, 4))
    ]
    password_list = password_letter_list + password_symbols_list +
password_numbers_list
    random.shuffle(password_list)

    password = "".join(password_list)
    pwd_entry.insert(0, password)
    copy_to_clipboard(password)


def save():
    """Save user info to the file system"""
    website = website_entry.get()
    username = username_entry.get()
    pwd = pwd_entry.get()

    # Form Validation
    if len(website) == 0 or len(username) == 0 or len(pwd) == 0:
        messagebox.showerror(
            title="Info", message="Please make sure to enter all fields"
        )

        return

    # Pop-up confirmation
    is_ok = messagebox.askokcancel(
        title=website,
        message=f"Is that ok to save {username} / {pwd} ?",
    )

    if is_ok:
        try:
            # Open a file with r mode
            # And load it
            with open("data.json", encoding="utf-8", mode="r") as file:
                data = json.load(file)
        except FileNotFoundError:
            # If there is a file not found exception
            # Then, open a file with w mode
            # And dump to json file directly since it will be the
# first record
            with open("data.json", encoding="utf-8", mode="w") as file:
                json.dump({website: {"email": username, "pwd": pwd}},
file, indent=4)
        else:
            # Continue from try clause
            # Update python object data with the new record
            data.update({website: {"email": username, "pwd": pwd}})

            # Open a file with w mode
            # And write updated python object back to json file
            with open("data.json", encoding="utf-8", mode="w") as file:
                json.dump(data, file, indent=4)
        finally:
            # Clear User Input
            website_entry.delete(0, tk.END)
            pwd_entry.delete(0, tk.END)


# Init
root = tk.Tk()
root.title("Password Manager")
root.config(padx=50, pady=50)

# Init canvas
canvas = tk.Canvas(width=200, height=200)
logo_image = tk.PhotoImage(file="logo.png")
canvas.create_image(100, 100, image=logo_image)
canvas.grid(column=1, row=0)

# Website
website_label = tk.Label(text="Website:")
website_label.grid(column=0, row=1)
website_entry = tk.Entry()
website_entry.grid(sticky="ew", padx=[0, 5], column=1, row=1)
# Focus entry
website_entry.focus()

# Search
search_btn = tk.Button(text="Search", command=find_password)
search_btn.grid(sticky="ew", padx=[5, 0], column=2, row=1)

# Email/Username
username_lable = tk.Label(text="Email/Username:")
username_lable.grid(column=0, row=2)
username_entry = tk.Entry()
username_entry.grid(sticky="ew", column=1, row=2, columnspan=2)
username_entry.insert(0, "frank@demo.com")

# Password
pwd_lable = tk.Label(text="Password:")
pwd_lable.grid(column=0, row=3)
pwd_entry = tk.Entry()
pwd_entry.grid(sticky="ew", padx=[0, 5], column=1, row=3)

# PWD Generation Btn
generate_pwd_btn = tk.Button(text="Generate Password",
command=generate_password)
generate_pwd_btn.grid(sticky="ew", padx=[5, 0], column=2, row=3)

# Add Btn
add_btn = tk.Button(text="Add", command=save)
add_btn.grid(sticky="ew", column=1, row=4, columnspan=2)


# Start the Event Loop
root.mainloop()