Wednesday, November 23, 2022

Python - Project - Quiz App (Day 34)

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.


Project - Quiz App



Revise the Quiz Project in Day 17.
    
#1 Call Trivia API to get the question instead of hard-coded in txt file
    
#2 Use Tkinter to improve the User Experience

#3 Review OOP Design



Ex: data.py
import requests

# Prepare parameters for API
parameters = {
    "amount": 10,
    "category": 18, # Science - Computers
    "type": 'boolean',
}
# Sends a GET request.
response = requests.get("https://opentdb.com/api.php", params=parameters)
# Raises HTTPError, if one occurred.
response.raise_for_status()
# Returns the json-encoded content of a response, if any.
res= response.json()
question_data = res["results"]


Ex: question_model.py
class Question:

    def __init__(self, q_text, q_answer):
        self.text = q_text
        self.answer = q_answer


Ex: quiz_brain.py
import html

class QuizBrain:

    def __init__(self, q_list):
        self.question_number = 0
        self.score = 0
        self.question_list = q_list
        self.current_question = None

    def still_has_questions(self):
        return self.question_number < len(self.question_list)

    def next_question(self):
        self.current_question = self.question_list[self.question_number]
        self.question_number += 1
        question_text = html.unescape(self.current_question.text)

        return f"Q.{self.question_number}: {question_text} (True/False): "

    def check_answer(self, user_answer: str) -> bool:
        correct_answer = self.current_question.answer
       
        if user_answer.lower() == correct_answer.lower():
            self.score += 1
            return True

        return False


Ex: ui.py
import tkinter as tk
from quiz_brain import QuizBrain

THEME_COLOR = "#375362"


class QuizInterface:
    def __init__(self, quiz_brain: QuizBrain):
        self.quiz_brain = quiz_brain

        # Init Tkinter
        self.root = tk.Tk()
        self.root.title("Quizzler")
        self.root.config(padx=20, pady=20, background=THEME_COLOR)

        # Score Counter
        self.score_label = tk.Label(
            text="Score: 0",
            foreground='white',
            background=THEME_COLOR)
        self.score_label.grid(row=0, column=1)

        # Init canvas
        self.canvas = tk.Canvas(
            width=300, height=250, background='white', highlightthickness=0
        )
        # Question Text
        self.question_text = self.canvas.create_text(
            150, 125, width=280, text="",
            fill="black", font=('Arial', 15, "italic")
        )
        self.canvas.grid(row=1, column=0, columnspan=2, pady=50)

        # True Btn
        true_image = tk.PhotoImage(file="images/true.png")
        self.true_btn = tk.Button(
            image=true_image, command=self.true_btn_clicked)
        self.true_btn.config(
            background=THEME_COLOR,
            activebackground=THEME_COLOR)
        self.true_btn.grid(row=2, column=0)

        # False Btn
        false_image = tk.PhotoImage(file="images/false.png")
        self.false_btn = tk.Button(
            image=false_image, command=self.false_btn_clicked)
        self.false_btn.config(
            background=THEME_COLOR,
            activebackground=THEME_COLOR)
        self.false_btn.grid(row=2, column=1)

        # Get the next question
        self.get_next_question()

        # Start the Event Loop
        self.root.mainloop()

    def get_next_question(self):
        # Config white bg for canvas
        self.canvas.config(background='white')

        # No more questions
        if not self.quiz_brain.still_has_questions():
            # Show game over info
            self.canvas.itemconfig(self.question_text, text="No more questions")
            # Disable buttons
            self.true_btn.config(state='disabled')
            self.false_btn.config(state='disabled')
            return

        # Not finish all questions yet
        quiz_text = self.quiz_brain.next_question()
        self.canvas.itemconfig(self.question_text, text=quiz_text)

    def true_btn_clicked(self):
        self.show_feedback(self.quiz_brain.check_answer("true"))

    def false_btn_clicked(self):
        self.show_feedback(self.quiz_brain.check_answer("false"))

    def show_feedback(self, is_correct: bool):
        # Config bg of canvas based on the correction of answer
        if is_correct:
            self.canvas.config(background='green')
        else:
            self.canvas.config(background='red')

        # Update Score Label
        self.score_label.config(text=f"Score: {self.quiz_brain.score}")

        # 1 second delay show the next question
        self.root.after(1000, self.get_next_question)


Ex: main.py
from question_model import Question
from data import question_data
from quiz_brain import QuizBrain
from ui import QuizInterface

question_bank = []
for question in question_data:
    question_text = question["question"]
    question_answer = question["correct_answer"]
    new_question = Question(question_text, question_answer)
    question_bank.append(new_question)

quiz = QuizBrain(question_bank)
quiz_ui = QuizInterface(quiz)


Sunday, November 6, 2022

Python - API endpoint and parameters (Day 33)

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.


Application Programming Interfaces



An Application Programming Interface (API) is a set of commands, functions, protocols, and objects that programmers can use to create software or interact with an external system.


HTTP Status Code



    1XX: Information
    2XX: Successful Response
    3XX: Redirection Message
    4XX: Client Error
    5XX: Server Error

Reference: Link


requests module



requests is an elegant and simple HTTP library for Python.


API Endpoint



It is an URL to tell our program that where to get data.

Ex: Get ISS Location through Web API
import requests

# Sends a GET request.
response = requests.get("http://api.open-notify.org/iss-now.json")

# Raises HTTPError, if one occurred.
response.raise_for_status()

# Returns the json-encoded content of a response, if any.
data = response.json()

print(data)


Result:
{
'iss_position': {'longitude': '37.1042', 'latitude': '-44.8634'},
'message': 'success', 'timestamp': 1667726746
}



Challenge - Kanye Quote App



Use Tkinter to build a GUI app, and integrate it with kanye.rest Web API to get the quote once the emoji is clicked.




Ex: 
from tkinter import *
import requests


def get_quote():
    """Get quote from kanye.rest web api"""

    # Sends a GET request.
    response = requests.get("https://api.kanye.rest/")

    # Raises HTTPError, if one occurred.
    response.raise_for_status()

    # Returns the json-encoded content of a response, if any.
    data = response.json()

    # Update canvas text
    canvas.itemconfig(quote_text, text=data["quote"])


# Init
root = Tk()
root.title("Kanye Says...")
root.config(padx=50, pady=50)

# Init canvas
canvas = Canvas(width=300, height=414)
background_img = PhotoImage(file="background.png")
canvas.create_image(150, 207, image=background_img)
quote_text = canvas.create_text(
    150,
    207,
    text="Kanye Quote Goes HERE",
    width=250,
    font=("Arial", 30, "bold"),
    fill="white",
)
canvas.grid(row=0, column=0)

# Button
kanye_img = PhotoImage(file="kanye.png")
kanye_button = Button(image=kanye_img, highlightthickness=0,
command=get_quote)
kanye_button.grid(row=1, column=0)

# Start the Event Loop
root.mainloop()



API Parameters



Able to give input to the Web API and get the data back based on the input.

Ex: Get sunrise and sutset info by the current location
import requests

MY_LAT = 49.282730
MY_LNG = -123.120735

# Prepare parameters for API
parameters = {"lat": MY_LAT, "lng": MY_LNG, "formatted": 0}

# Sends a GET request.
response = requests.get("https://api.sunrise-sunset.org/json",
params=parameters)

# Raises HTTPError, if one occurred.
response.raise_for_status()

# Returns the json-encoded content of a response, if any.
data = response.json()

print(data)


Result:
{
'results': {
'sunrise': '2022-11-06T15:07:22+00:00',
'sunset': '2022-11-07T00:44:52+00:00',
'solar_noon': '2022-11-06T19:56:07+00:00',
'day_length': 34650,
'civil_twilight_begin': '2022-11-06T14:35:13+00:00',
'civil_twilight_end': '2022-11-07T01:17:01+00:00',
'nautical_twilight_begin': '2022-11-06T13:57:13+00:00',
'nautical_twilight_end': '2022-11-07T01:55:01+00:00',
'astronomical_twilight_begin': '2022-11-06T13:20:05+00:00',
'astronomical_twilight_end': '2022-11-07T02:32:09+00:00'
}, 'status': 'OK'
}



Project - ISS Overhead Notifier



#1 Get ISS location by Web API
http://open-notify.org/Open-Notify-API/ISS-Location-Now/

#2 Get your city Location
https://www.latlong.net/

#3 Determine it is night by Web API
https://sunrise-sunset.org/api

#4 If ISS is overhead and it is night time, send an eamil to yourself to notify you to look up

#5 Run #4 every 60 seconds

Ex:
import requests
from datetime import datetime
import smtplib
import time

GMAIL_SMTP_SERVER_ADDRESS = "smtp.gmail.com"
GMAIL_SMTP_TLS_PORT = 587
GOOGLE_APP_PWD = "your google app pwd"
MY_EMAIL_ADDRESS = "your email address"
MY_LAT = 49.282730
MY_LONG = -123.120735


def send_email(receiver, message):
    """Send an custom email to the receiver"""

    # 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=GOOGLE_APP_PWD)

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


def get_iss_location():
    """Get ISS current location from web api"""

    # Sends a GET request.
    response = requests.get(url="http://api.open-notify.org/iss-now.json")
    # Raises HTTPError, if one occurred.
    response.raise_for_status()
    # Returns the json-encoded content of a response, if any.
    return response.json()


def is_iss_overhead():
    """Determine if ISS is overhead or not"""

    data = get_iss_location()
    iss_latitude = float(data["iss_position"]["latitude"])
    iss_longitude = float(data["iss_position"]["longitude"])

    # Your position is within +5 or -5 degrees of the ISS position.
    if (
        iss_latitude - 5 <= MY_LAT <= iss_latitude + 5
        and iss_longitude - 5 <= MY_LONG <= iss_longitude + 5
    ):
        return True

    return False


def get_sunrise_and_sunset_info(lat, lng):
    """Get the sunrise and sunset info from web api"""

    # Prepare parameters for API
    parameters = {
        "lat": lat,
        "lng": lng,
        "formatted": 0,
    }
    # Sends a GET request.
    response = requests.get("https://api.sunrise-sunset.org/json",
params=parameters)
    # Raises HTTPError, if one occurred.
    response.raise_for_status()
    # Returns the json-encoded content of a response, if any.
    return response.json()


def is_night_time():
    """Determine if it is night time from the current location"""

    data = get_sunrise_and_sunset_info(MY_LAT, MY_LONG)
    sunrise = int(data["results"]["sunrise"].split("T")[1].split(":")[0])
    sunset = int(data["results"]["sunset"].split("T")[1].split(":")[0])

    time_now = datetime.now()

    # If it is dard, then send an email to myself (Look Up)
    if time_now.hour < sunrise or time_now.hour > sunset:
        return True

    return False


while True:
    # Delay 60 seconds
    time.sleep(60)

    # Send an email if iss is overhead and it is night time
    if is_iss_overhead() and is_night_time():
        send_email(
            MY_EMAIL_ADDRESS,
            "Subject: Look Up\n\nISS is overhead!\n",
        )


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