Tuesday, November 1, 2022

Python - smtplib and datetime modules (Day 32)

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.


Setting up a Google Account



In order to use Gmail SMTP Server,  we need to change some settings of Google Accounts

We can enable "less secure apps" feature in Google and our python code can use this account's credential to send out emails.

Unfortunately, it is no longer available.


The alternative way is to use an App Password.


An App Password is a 16-digit passcode that gives a less secure app or device permission to access your Google Account. App Passwords can only be used with accounts that have 2-Step Verification turned on.

After enabling 2-Step Verification, we can see 'App passwords' option under Signing in to Google section.


Then we can create app passwords to be used by our python code instead of google account password.



smtplib module





Ex:
import smtplib

GMAIL_SMTP_SERVER_ADDRESS = "smtp.gmail.com"
GMAIL_SMTP_TLS_PORT = 587
MY_EMAIL_ADDRESS = "your_account@gmail.com"

# Create a connection to SMTP Provider
connection = smtplib.SMTP(
host=GMAIL_SMTP_SERVER_ADDRESS,
port=GMAIL_SMTP_TLS_PORT
)

# Make this connection secure
connection.starttls()

# Login with your Google App Password
connection.login(user=MY_EMAIL_ADDRESS, password="your_app_password")

# Send an email
connection.sendmail(
    from_addr=MY_EMAIL_ADDRESS,
    to_addrs="your_receiver@gmail.com",
    msg="Subject: Python smtplib exp\n\nHello World!\n",
)

# Close connection
connection.close()


Result:



Good Article to read:



datetime module





Ex:
import datetime as dt

# Return the current local date and time.
now = dt.datetime.now()

# <class 'datetime.datetime'>
print(type(now))

# 2022-11-01 02:55:54.186915
print(now)

# 2022
print(now.year)

# 11
print(now.month)

# 1
print(now.day)

# Return day of the week, where Monday == 0 ... Sunday == 6.
# 1
print(now.weekday())


# Create a custom datetime
my_birthday = dt.datetime(year=1999, month=12, day=1)

# 1998-12-01 00:00:00
print(my_birthday)


Challenge - Send Motivational Quotes on Mondays via Email



#1 Use datetime module to determine if the current day is Monday
#2 Open 'quotes.txt' file to get the all quotes
#3 Use random module to pick one quote randomly
#4 Use smtplib to send out Monday Motivation


quotes.txt
"When you arise in the morning think of what a privilege it is to be alive,
to think, to enjoy, to love..." - Marcus Aurelius "Either you run the day or the day runs you." - Jim Rohn

Ex:
import random
import datetime as dt
import smtplib

GMAIL_SMTP_SERVER_ADDRESS = "smtp.gmail.com"
GMAIL_SMTP_TLS_PORT = 587
MY_EMAIL_ADDRESS = "your_account@gmail.com"


def get_quote():
    """Open quotes.txt and randomly pick one quote"""

    # Open file
    with open("quotes.txt") as file:
        # Use random module to pick one quote
        quote = random.choice(file.readlines())

    return quote


def is_monday():
    """Check if the current day is Monday"""

    now = dt.datetime.now()
    return now.weekday() == 0


def send_email(message):
    """Send an email with message"""

    # Create a connection to SMTP Provider
    with smtplib.SMTP(
        host=GMAIL_SMTP_SERVER_ADDRESS, port=GMAIL_SMTP_TLS_PORT
    ) as connection:

        # Make this connection secure
        connection.starttls()

        # Login with your Google App Password
        connection.login(
user=MY_EMAIL_ADDRESS,
password="your_app_password"
)

        # Send an email
        connection.sendmail(
            from_addr=MY_EMAIL_ADDRESS,
to_addrs="your_receiver@gmail.com",
msg=message
        )


# if it is Monday, then we will send a quote to motivate ourselves
if is_monday():
    quote = get_quote()

    send_email(f"Subject: Monday Motivation\n\n{ quote }\n")


Project - Automated Birthday Wisher



#1 Fill out bithdays.csv with 'name,email,year,month,day' format
#2 Use pandas module to read csv
#3 Use list comprehension for filtering to get the birthdays which match today
#4 Open file and use random module to get the letter template
#5 Use smtplib module to send out birthday emails

letter_templates/letter_1.txt: (1~3)
Dear [NAME], Happy birthday! All the best for the year! Frank

birthdays.csv
name,email,year,month,day Mom,mom@your_email_provider,2022,11,01 Dad,dad@your_email_provider,2022,11,02

Ex:
import datetime as dt
import pandas
import random
import smtplib

GMAIL_SMTP_SERVER_ADDRESS = "smtp.gmail.com"
GMAIL_SMTP_TLS_PORT = 587
MY_EMAIL_ADDRESS = "your_account@gmail.com"

now = dt.datetime.now()
birthdays_data_frame = pandas.read_csv("birthdays.csv")


def send_email(email_address, message):
    """Send an email with message"""

    # Create a connection to SMTP Provider
    with smtplib.SMTP(
        host=GMAIL_SMTP_SERVER_ADDRESS, port=GMAIL_SMTP_TLS_PORT
    ) as connection:

        # Make this connection secure
        connection.starttls()

        # Login with your Google App Password
        connection.login(user=MY_EMAIL_ADDRESS,
password="your_app_password")

        # Send an email
        connection.sendmail(
            from_addr=MY_EMAIL_ADDRESS,
to_addrs=email_address,
msg=message
        )


def get_content_from_an_random_letter():
    """Get random letter content"""

    # Pick a random letter
    random_letter_path =
f"letter_templates/letter_{random.randint(1,3)}.txt"

    # Open letter template file and return its content
    with open(random_letter_path) as file:
        return file.read()


# List Comprehension
# Get the birthdays if its date match today
match_birthdays = [
    row
    for (_index, row) in birthdays_data_frame.iterrows()
    if row["month"] == now.month and row["day"] == now.day
]

# Loop through birthdays and send emails
for person in match_birthdays:

    # Generate birthday message randomly by template
    message = get_content_from_an_random_letter().replace("[NAME]",
person["name"])

    # Send an email
    send_email(person["email"],
f"Subject: Happy Birthday\n\n{ message }\n")


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