Python Lab: Scratchpads

Python Lab: Scratchpads

Most of the code is commented. Please uncomment sections/blocks in order to use them.

# Data Types
#
# bool - True or False values
# int - 3, 45, 23
# str - "Alan Turing", "a", "x", "123"

# list = [0: "alan turing", 1: "stephen hawking", 2: 12, 4, 5.99, True, [1, 2, 3, 4, 5], False]
# dict = {"first_name": "Rahul", "last_name": "Sharma", "job": "Developer & Trainer", "dob": 12, "sleepy": False, data: [1, 2, 3, 4]}

# Dynamic typing
# Static typing

# name = "Amit"
# print(type(name))

# name = "Mayuresh"
# print(type(name))

# name = 100
# print(type(name))


# # file 1
# total_items = 10


# # file 1
# processed_total_items = "10 items"
# total_items = processed_total_items


# # file 1
# gross_amount = "10 items" + 3
# # ERROR


# last_name = None

# # --------------------

# first_name = 99
# last_name = None
# phone = "98213213"
# subscription = None

# quote = "Linux at OSSCON said, 'Talk is cheap, show me the code.'"
# print(quote)


# \n	Enter / Return
# \'	Single quote
# \"	Double quote
# \t	Horizontal Tab
# \v	Vertical Tab
# \\	Backslash

# name = "alan\n\n\nturing"
# name = "alan\vturing"
# print(name)

# concatenation
welcome_msg = "Howdy"
name = "Alan"

# print(welcome_msg + ", " + name)
# print(2 + 2)

# name = "Stephen" * 5
# print((welcome_msg + " " + name + "\n") * 1000)

# Formatted string (Interpolation)

# print(welcome_msg + ", " + name + "!")
time = 10
new_time = 30

# print(f"Hello, {type(name)}")

# print(welcome_msg + ", " + name + "! How are you?" + " It's " + str(10000) + "PM")

# # f strings
# print(f"{welcome_msg}, {name}! How are you? It's {(time + new_time) // 2}PM")

# # .format() method
# output = "{}, {} {}! How are you?".format(welcome_msg, 1000 * 10, time)

# # print() function
# print(output)


# Built in methods and functions

# name = "Stephen {}"
# print(name.format("Hawkwing"))

# print("100 - {}".format("Hawking"))

# print(type(num))

# items_in_cart = 8
# msg = "You have %d items in your shopping cart." % (items_in_cart)

# print(msg)


# "hello"[0]

# name = "Stephen Hawkin"
# name[4]

# integer = 10
# bool_value = True
# list = [True, True, False, 10, "Hello"]

# print(integer * 1.2)

# print("The answer is " + str(list))

# print("Type something please:")
user_input = input("Type something please\n")

print(f"You typed {user_input} - {type(int(user_input))}")
# = assignment
# == compare

# name = input("Please enter your name: ")

# if name != "Mark Zuckerberg":
#     print("You don't belong to Facebook")
# elif name == "Jack Dorsey":
#     print("You don't belong to Twitter")
# elif name == "Sundar Pichai":
#     print("You don't belong to Google")
# else:
#     print("Whatever!")

# Operators
# == equal to
# != not equal to
# > great than
# < less than
# >= greater than or equal to
# <= less than or equal to

# age = input("Please enter your age: ")
# age = int(age)

# if age >= 21:
#     print("You are allowed to drink")
# elif age >= 65:
#     print("You get drinks for free")
# else:
#     print("Sorry you can't drink!")

# if items > 20:
#     print("Oh, you have a lot of stuff")
# elif items < 10:
#     print("Too bad, you can buy more.")
# else:
#     print("That's perfect")

# Truthiness
# Falsyness

# logged_in = "someuser@gmail.com"

# if logged_in:
#     print("You have logged in, see the content")
# else:
#     print("Sorry, please login to check")

# Logical Operations
# and
# or
# not

# age = input("Please enter your age: ")
# age = int(age)

# if age >= 21 and age < 65:
#     print("You are allowed to drink")
# elif age >= 65:
#     print("You get drinks for free")
# elif age >= 18 and age < 21:
#     print("You can enter but you cannot drink")
# else:
#     print("Sorry you can't drink, you're too young!")

# NAME
# city = "mumbai"

# if city == "mumbai" or city == "bangalore":
#     print("You live in a tier 1 city")

# if city == "pune" or city == "jaipur":
#     print("You live in a tier 2 city")

# if not city == "mumbai":
#     print("You are out of Mumbai")

# and -> LHS and RHS  TRUE
# 0, None, "" -> Falsy values
#

# username = "mark@fb.com"

# if not username:
#     print("Hello my friend")
# else:
#     print("You are not welcome")


# is vs ==
# == - it checks for the value.
# is - checks whether it is the same thing in memory


# Nested conditionals

age = input("How old are you? ")

if age:
    age = int(age)
    if age >= 21 and age < 65:
        print("You are allowed to drink")
    elif age >= 65:
        print("You get drinks for free")
    elif age >= 18 and age < 21:
        print("You can enter but you cannot drink")
    else:
        print("Sorry you can't drink, you're too young!")
else:
    print("Please enter a valid age!")
# For loop

# numbers = [100, 20, 2, 24, 45]

# for number in numbers:
#     num = (number / 2) * 2.3
#     print(f"The value of this number is {num}")


# name = "Stephen"

# for char in name:
#     print(char)

# Iterable - range of numbers, strings, lists, dictionaries
# my_time = 4

# for time in [1, 2, 3, 4]:
#     print("Happy Birthday!")
#     my_time = time


# print(my_time)
# for item in iterable_object:
#     do something

# for num in 10:
#     print("Hello World")

# range(20) -> 0
# range(10, 21) ->
# range(0, 21, 2)
# range(10, 1, -1)
# range(start = 0, end, step = 1)

# for time in range(0, 6):
#     print("JUMP")

# rant = input("How many times do you want to scream? ")
# rant = int(rant)

# for i in range(0, -50, -1):
#     print("CLEAN YOUR ROOM!!!!")

#     if i == 5:
#         print("OKAY OKAY, I'll do it!")

# 1 - 20 ->
# odd - "Fizz is odd"
# even - "Fizz is even"
# 5, 16, print("FizzBuzz")

# num % 2 != 0 (odd)

# for num in range(1, 21):
#     if num == 5 or num == 16:
#         print(f"{num}: FizzBuzz")
#     elif num % 2 == 0:
#         print(f"{num}: Fizz is even")
#     elif num % 2 != 0:
#         print(f"{num}: Fizz is odd")

# While loop

# password = input("Please enter your secret password: ")

# while password != "tesla1":
#     print("INCORRECT PASSWORD! GET LOST!")
#     password = input("Please enter the correct password: ")

# print("CORRECT! Hello...")


# num = 10

# i = 1
# while i <= num:
#     print(f"{i}: Hello World")
#     i = i + 1

# break statement

# password = input("Please enter your secret password: ")

# while password != "tesla1":
#     if password == "GODMODE":
#         break

#     print("INCORRECT PASSWORD! GET LOST!")
#     password = input("Please enter the correct password: ")

# print("CORRECT! Hello...")


# num = 30

# i = 1
# j = 1
# while i <= num:
#     if i == 10:
#         while j < 5:
#             print("Hello world!")
#             j += 1

#     print(f"{i}: Hello World")
#     i = i + 1


# Exercise 1
# 0 : 20
# 0 : -30
# 30 : -30


# Exercise 2: Rock paper scissor
# best out of three

# player1_wins = 3
# player2_wins = 0
# winning_score = 3


# while player1_wins >= 3 or player2_wins >= 3:
#     print("Player1 wins")
#     player1_wins += 1

#     # code...............

# if player1_wins > player2_wins:
#     print("PLAYER 1 is the victor!")
# else:
#     print("PLAYER 2 is the vector!")


# Data Structures -> Data Types
# int, strings, None, List, Dicts

# data structure
# list = [1, "hello", 3, 4, True, None, [1, 2, 3, 4], [1, 2, 3, 5.5555]]

# shopping_cart = ["apples", "cucumbers", "phone"]


# results = [
#     ["One", "Metallica", 3.49, 1000000, "Song performed by metallica from the album, St. Anger",
#      "bucket3.aws.amazonaws.com/photo-gal/11134/32/thumbnail_32.png"]
#     ["Fade to black", "Metallica", 3.49, 1000000, "Song performed by metallica from the album, St. Anger",
#      "bucket3.aws.amazonaws.com/photo-gal/11134/32/thumbnail_32.png"]
#     ["Fade to black", "Metallica", 3.49, 1000000, "Song performed by metallica from the album, St. Anger",
#      "bucket3.aws.amazonaws.com/photo-gal/11134/32/thumbnail_32.png"]
# ]
# # Lists / Arrays

# shopping_cart = ["Mi Smart Band 3i", "Asian Century-06",
#                  "Gilma 14558-GA Digital", True, 234]

# calculate_items = len(shopping_cart)

# # print(f"My Cart({calculate_items})")

# for product in shopping_cart:
#     print(item)


# # List methods

# todos = ['lampstack.py', 'configuring_mysql.py', 'reverse_procy', 'reverse_proxy']

# .append(value)    // one argument
# .extend(list)     // a list
# .insert(0, value) // 2 arguments
# .clear()          // no arguments, delete all the values
# .pop(index?)      // remove a value from a particular index [returns]
# .remove(value)    // delete the very first occurrence
# .count()          // returns the number of times a value appears in the list
# .reverse()        // reverse the elements of the list
# " ".join(list)    // create a new strings by combining the values of a list
# list[start:end:step]  // all arguments are optional, but you have add the colons
# [item logic for item in list]

# mayuresh = [1,2,3]
# amit = [1,2,3]

# [1,2,3].append(4)


# your_name = input("Please enter your name ").lower()

# print(your_name)


# received_token = ["Bearer", "qw23421o3i92u49832u49ijnwdeijesadasda"]

# amit_token = " ".join(received_token)

# amit_token = "Bearer qw23421o3i92u49832u49ijnwdeijesadasda"


# # Slicing

# nums = [1, 2, 3, 4, 5, 6]

# nums[start:end:step]


# List Comprehension
# nums2 = [2, 4, 6]

# nums = [1, 2, 3]

# nums2 = []
# for num in nums:
#     num2.append(num * 2)


# # nums2 = [(num*2) + 1 for num in nums]

# nums2 = [num*2 for num in nums]


# [item logic for item in list]
names = ["alan", "lisa", "john", "jack"]
uppercased = [name.upper() for name in names]
print(uppercased)


# For loop
numbers = [1, 2, 3, 4, 5]
doubled_numbers = []

for num in numbers:
    doubled_num = num * 2
    doubled_numbers.append(doubled_num)

print(doubled_numbers)


# Comprehension
numbers = [1, 2, 3, 4, 5]
doubled_numbers = [num * 2 for num in numbers]
print(doubled_numbers)
# # List Comprehensions
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

[print(num*200) for num in numbers]

# if/else conditionals inside comprehensions

[print(num) for num in numbers if num % 2 == 0]

for num in numbers:
    if num % 2 == 0:
        print(num)


names = [
    "alan",
    "mary",
    "joe",
    "jack",
    "toby",
    "joe",
    "edgar",
    "joe"
]

[print(f"Hi, I am {name}") for name in names if name == "joe"]

"".join(names)

my_name = "Mayuresh Badgujar"

"alan" in names

# my_name[2]
"".join(letter*4 for letter in my_name)


for letter in my_name:
    print(letter*4)


# Nested Lists

nested_list = [[1, 2, 3, ["alan", "lisa"]], [4, 5, 6], [7, 8, 9]]


nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# print(nested_list)

# nested_list[2][2]  # 9

for x in nested_list:
    for y in x:
        print(y)

x = [7, 8, 9]


# Nested list comprehensions
# Looping in comprehensions


[[print(val) for val in nested_list] for list in nested_list]

[[print(val) for val in item] for item in nested_list]


configurations = ["192.189.32.4", "186.180.32.3"]


# Dictionaries

addresses = {
    "amit": "192.189.32.4",
    "mayuresh": "186.180.32.3",
    "john": "192.134.86.1:11001"
}

# key-value pair

product = {
    "name": "AMD Ryzen Threadripper 3990X",
    "no_of_cores": 64,
    "no_of_threads": 128,
    "unlocked": True,
    "price": 333999.00
}

# product = [True, "AMD Ryzen Threadripper 3990X", 64, 128]

product_name = product["name"]
product_cores = product["unlocked"]


my_dict = {"name": "alan", "achievement": "turing machine", "nobel": 0}
my_dict = dict(name="alan", achievement="turing machine", nobel=0)

my_dict["nobel"]


# Iterate over a dictionary

for value in my_dict.keys():
    print(value)

for value in my_dict.values():
    print(value)

# Methods
# .keys()
# .values()
# .items()


for k, v in my_dict.items():
    print(f"My {k} is {v}")

# by default it will assume we are searching in the keys
"name" in my_dict

"alan" in my_dict.values()

# (key:value)
# Dictionary Methods

# .clear()
my_dict = {"name": "alan", "achievement": "turing machine", "nobel": 0}
my_dict.clear()

# copy()
my_dict2 = {"key1": [1, 2, 3, "alan", "turing"], "key2": 12334}
duplicate_dict = my_dict2.copy()

# fromkeys()
my_list = ["alan", "lisa", "mary"]


{}.fromkeys(my_list, True)


# .get()
my_dict.get("name")

# .pop()
my_dict2.pop("song")


my_dict2["key2"] = 213234234
my_dict2["something"] = False


song1 = {"name": "Schism", "artist": "Tool"}
song2 = {}
song3 = {"name": "Dil Bechara", "artist": "Arijit"}

song2.update(song1)
song2.update(song3)


# Dictionary Comprehesion

{1: 1, 2: 8, 3: 27, 4: 64}

{num: num ** 3 for num in [1, 2, 3, 4]}

[True, 1, "alan", {"name": "alan"}]

# APIs

[
    {
        "name": "Parabola",
        "artist": "Tool"
    },
    {
        "name": "Schism",
        "artist": "Tool"
    },
    {
        "name": "Schism",
        "artist": "Tool"
    },
]
# # Dictionary Comprehesion
# profile = {"name": "rahul", "city": "mumbai", "job": "trainer and programmer"}

# print({print(key, value) for key, value in profile.items()})


# {value.upper() for value in profile.values()}

# Tuple

# [] {} ()

# numbers1 = [1, 2, 3, 4]  # list

# # immutable data structure
# numbers2 = (1, 2, 3, 4)  # tuple


# country = ('united states', 'india', 'uk', 'uae', 'france', 'india', 'india', 'france', 'china')


# for x in country:
#     print(x)


# List comprehensions with if/else conditionals
# numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# [print(num*2) if num % 2 == 0 else print(num/2) for num in numbers]

# # [num for num in numbers if num % 2 == 0]


# # [num*2 if num % 2 == 0 else num/2 for num in numbers]


# for num in numbers:
#     if num % 2 == 0:
#         print(num * 2)
#     else:
#         print(num / 2)

# 0.5
# 4
# 1.5
# 8
# 2.5
# 12
# 3.5
# 16
# 4.5
# 20


# numbers = (1, 2, 3, 4, 4, 2, 4, 6, 2)
# numbers.count(4)

# numbers.count(4)
# numbers.index(2)


# # Sets
# # sets cannot contain duplicate values
# # they aren't orders

# num_set = {1, 2, 3, 4}

# num_set = set({1, 2, 3, 4, 5, 5, 5, 6})

# num_set.remove(5)
# num_set.remove(3)
# num_set.discard(1)  # will not throw an error if value doesn't exist in set
# num_set2 = num_set.copy()
# num_set.clear()  # clears the set, deleted all the values in the set

# num_set2 = num_set.copy()


# # .intersection (&)
# # .symmetric_difference (^)
# # .union (|)


# set1 = {1, 2, 3, 6, 7, 10, 44, 14, 15}
# set2 = {3, 6, 7, 10, 54, 12, 45, 938}


# # Functions
# [1, 2, 3].pop(3)
# print("Text")

# # A process for executing a task
# # It can accept input and return an output
# # Usefull for executing certain tasks over and over


# def my_function():
#     # code block


# my_function()


# def say_hello():
#     return "Hello World!"


# result = say_hello()

# print(result)


# def square_of_7():
#     return 7 ** 2


# print(square_of_7)


# Passing arguments

# def add(a, b):
#     return a + b


# def greet(name):
#     return f"Hello, {name}"

# Parameter vs Argument

# def several_greets(name1, name2, name3, name4):
#     uppercased_name1 = name1.upper()
#     uppercased_name2 = name2.upper()
#     uppercased_name3 = name3.upper()
#     uppercased_name4 = name4.upper()

#     return "What's up!"


# # returned_result = greet(2333)

# print(several_greets("Amit", "Mayuresh", "Alan", "Mary"))


# def to_the_power_of(number, power):
#     return number ** power


# print(to_the_power_of(2, 5))


# def print_full_name(first_name, last_name):
#     return f"My first name is {first_name} and my surname is {last_name}"


# # keyword arguments
# print(print_full_name(last_name="singh", first_name="amit"))


# def sum_odd_numbers(numbers=[1,43,35,6,3,3,452,3]):
#     total = 0

#     for num in numbers:
#         if num % 2 != 0:
#             total += num

#     return total


# print(sum_odd_numbers([1, 2, 3, 4, 5, 6]))


# def expo(number, power=2):
#     return number ** power


# print(expo(9, 20))

def add(a, b):
    return a + b


def subtract(a, b):
    return a - b


def math(a, b, fn):
    return fn(a, b)


print(math(100, 200, subtract))


print(subtract)
# Python treats functions as values


# DIY
# 1. Create a function that accepts an argument and then capitalize the first
# capitalize("rahul")
# Hint: Use Slicing

# 2. Create a function that accepts a `list` multiple all the even numbers and return product
# multiply_even_nums([1,2,3,4,5,6,7,8,9])
# """ """ -> docstring
# from random import random


# def do_this(a, b):
#     """This do_this function accepts 2 arguments and it return just a string named "Hello"!"""
#     return "Hello!"


# print(random.__doc__)


# def add(a, b):
#     return a + b


# def math(a, b, fn):
#     return fn(a, b)


# print(math(10, 10, add))

# *args
# We put *args as a parameter to a function definition. It will collect any number of additional
# or extra arguments that has been passed in, into a single tuple named args (whatever you choose to name it)

# def add_all(*args):
#     total = 0
#     for num in args:
#         total += num

#     return total


# print(add(10, 20, 40, 3, 4, 54, 89))


# def add_all(name, *args):
#     total = 0
#     for num in args:
#         total += num

#     return f"{name}'s score = {total}"


# print(add_all("alan", 10, 20, 40, 3, 4, 54, 89))


# Another example

# def check_authority(*params):
#     if "admin" in params or "owner" in params:
#         return "You can access this content!"

#     return "Access denied!"


# print(check_authority("hello", "world", 234, True))


# **kwargs - Will collect all remaining keyword arguments as a dictionary.
# {"name": "alan", "age":30}

# def user_details(name, **kwargs):
#     for k, v in kwargs.items():
#         print(f"###{name}### - {k.upper()} - {v}")


# user_details(name="locke", first_name="John", last_name="Doe",
#              age=30, occupation="Software Engineer")


# Parameter ordering

# 1. normal parameters
# 2. *args
# 3. default parameters
# 4. **kwargs

# def show_info(a, b, *args, role="moderator", **kwargs):
#     return [a, b, args, role, kwargs]


# print(show_info(1, 2, 3, 4, first_name="john", second_name="locke"))


# [1, 2, (3, 4), 'moderator', {'first_name': 'john', 'second_name': 'locke'}]


# Argument unpacking

# Tuple/List Unpacking
# def add_all_values(*args):
#     total = 0
#     for num in args:
#         total += num

#     print(total)


# # add_all_values(1, 2, 34, 5, 64, 33)

# nums = (1, 2, 34, 5, 64, 33)

# add_all_values(*nums)


# Dictionary/Set Unpacking

# def say_name(first, last, **kwargs):
#     print(f"My name is {first} {last}.........{kwargs}")


# details = {"first": "Robert", "last": "Moore"}

# say_name(**details, color="red", age=25, occupation="astronomer")


# Lambda
# function / expression -> anonymous functions

# Lambda is just a function that has no name!
# We could save it to a variable as a normal value

# lambda num: num * num
# lambda functions will always return the value
# Lambdas only allow us to write a single expression. We cannot write multiple lines of code/logic.

# def multiply(num):
#     return num * num

# square = lambda num: num * num

# square(10)

# def add(a, b):
#     return a + b


# def multiply(a, b):
#     return a * b


# def divide(a, b):
#     return a / b


# def expo(a, b):
#     return a ** b


# def math(a, b, fn):
#     return fn(a, b)


# # print(math(10, 5, expo))


# print(math(10, 5, lambda a, b: a ** b))


# map()
# Accepts 2 args, function and an iterable

# iterables - strings, lists, tuples, dictionaries, sets, range(start, end, step)

# map(function, iterable)

# nums = [1, 2, 3, 4, 5, 6, 7, 8]

# squares = map(lambda a: a*a, nums)

# print(squares)
# print(list(squares))

# fav_num = input("Please enter your favorite no. ")
# a = int(fav_num) + 10


# filter()

# my_list = [1, 2, 3, 4]

# evens = list(filter(lambda x: x % 2 == 0, my_list))

# print(evens)


####### EXERCISES ########
# Capitalize the first letter in a word

def capitalize(word):
    return word[0].upper() + word[1:]


print(capitalize("amit"))


# Multiply even numbers -> list
def multiply_even_numbers(list):
    evens = [num for num in list if num % 2 == 0]

    result = 1
    for even in evens:
        result = result * even

    return result


print(multiply_even_numbers([1, 2, 3, 4, 5, 6]))
import pdb

# # names = ['John', 'Jack', 'James', 'Desmond', 'Charlie', 'Jacob']


# # # list()
# # result = list(filter(lambda value: "|||" + value + "|||", "Amit"))
# # print("-----".join(result))

# # def some_func(name):
# #     return len(name) < 5

# # some_func("Alan")

# # some_func2 = lambda name: len(name) <= 5

# # some_func2("Edgar")


# # MAP & FILTER

# users = [
#     {"username": "john", "tweets": ["Marcus was correct", "I am the danger"]},
#     {"username": "sawyer", "tweets": []},
#     {"username": "beth", "tweets": ["This world feels different"]},
#     {"username": "maggie", "tweets": [
#         "What about my family? my home? my village...? "]},
#     {"username": "darryl", "tweets": []},
# ]


# # Python way of doing
# [user["username"].upper() for user in users if not user["tweets"]]

# # inactive_users = list(filter(lambda t: len(t['tweets']) == 0, users))

# # print(inactive_users)


# # inactive_users = list(filter(lambda t: len(t['tweets']) == 0, users))
# # # print(inactive_users)
# # # [{'username': 'sawyer', 'tweets': []}, {'username': 'darryl', 'tweets': []}]

# # result = list(map(lambda d: d["username"], inactive_users))
# # # ['sawyer', 'darryl']

# # Other language use
# result2 = list(map(lambda d: d["username"].upper(), list(
#     filter(lambda t: len(t['tweets']) == 0, users))))

# # print(result2)


# # ╔══════════════════════════════════════════════════════════╗
# #         ▂▃▅▇█▓▒░ Built in functions ░▒▓█▇▅▃▂
# # ╚══════════════════════════════════════════════════════════╝
# # all() - Return True if all elements of an iterable are truthy (or if any of the iterable value if empty)

# people = ['John', 'Jack', 'Aames', 'Joule', 'Jane', 'Jacob']

# print(all([name[0] == 'J' for name in people]))
# print(all(["", ""]))


# # any() - Returns True if any of the elements of an iterable is truthy

# # sorted() - Returns a new sorted list from the items in the iterable

# nums = (2,3,7,1,9,1)

# sorted(nums)

# # min() max()
# max([2,34,23,2])
# min([2,34,23,2])

# # reversed()

# reversed([1,2,3,4])

# # len()

# # abs
# abs(5)

# # sum
# sum([1,2,3,4], 10)

# # def reverse_word(word):
# #     return "".join(list(reversed(word)))

# # round()

# zip()

# num1 = [1, 2, 3, 4]
# num2 = [11, 12, 13, 14, 15, 16]
# num3 = ["word", "hello", "world"]

# print(list(zip(num1, num2, num3)))
# [(1, 11), (2, 12), (3, 13), (4, 14), (5, 15), (6, 16)]

# [(1, 11, 21), (2, 12, 22), (3, 13, 434234), (4, 14, 32324), (5, 15, 324234), (6, 16, 434)]
# [(1, 11, 21), (2, 12, 22), (3, 13, 434234), (4, 14, 32324)]


# sem1 = [90, 65, 56]
# finals = [97, 62, 42]
# student = ['alan', 'ben', 'carl']

# # {'alan': 97, 'ben': 65, 'carl': 56}

# # print(list(zip(student, sem1, finals)))
# # [('alan', 90, 97), ('ben', 65, 62), ('carl', 56, 42)]

# # print({t[0]: max(t[1], t[2]) for t in zip(student, sem1, finals)})
# # {'alan': 97, 'ben': 65, 'carl': 56}

# print(dict(map(lambda t: [t[0], max(t[1], t[2])], zip(student, sem1, finals))))

# print(dict(zip(student, map(lambda t: max(t), zip(sem1, finals)))))


# Error

# Syntax Error
# Indentation Error
# Name Error
# Type Error
# Index Error
# Value Error
# Key Error
# Attribute Error

# All error list - https://docs.python.org/3/library/exceptions.html

# raise ValueError('invalid value')
# raise TypeError('invalid value')
# raise IndexError

# def colorize(text, color):
#     colors = ("red", "yellow", "blue", "green")
#     if type(text) is not str:
#         raise TypeError('Text must be a string')
#     if type(color) is not str:
#         raise TypeError('Color must be a string')
#     if color not in colors:
#         raise ValueError("Color is invalid color")

#     return f"Printed {text} in {color}"


# # colorize(234, "red")

# # Handle Error

# # try
# # except
# # else
# # finally

# try:
#     # attempt
#     result = colorize("hello", "vegetable")
# except:
#     # if unsuccessful
#     print("Something went wrong")
# else:
#     # if successful
#     print(result)
# finally:
#     print("This will always run no matter what")


# print("This is printer later on")

# pdb - Python Debugger
#  pdb.set_trace()


# try:
#     user_input = input("Please enter your choice: ")
# except:
#     print("Please enter a valid number")

user_input = input("Please enter your choice: ")

if [] in ['1', '2', '3', '4', '5', '6', '9', '0']:
    raise ValueError("Please enter either 'rock', 'paper' or 'scissor'")

if not (user_input == 'rock' or user_input == 'paper' or user_input == 'scissor'):
    raise ValueError("Please enter either 'rock', 'paper' or 'scissor'")

Scratchpad #9

# String methods
# name = "edgar alan poe"

# name.capitalize()
# name.count("a")
# name.center(20)
# name.find("a")
# name.isalnum()
# name.isalpha()
# name.isdecimal()
# name.isdigit()
# name.rfind('a')  # -> search index from last
# name.rindex("ar")
# name.strip()  # -> kinda like trim
# name.title()  # -> first character of every word
# name.split()  # split string into a list

# name.strip().title()

# https://docs.python.org/3/library/stdtypes.html#str.capitalize


# # import random
# # import random as surprise
# # from random import randint
# # from random import random as rand
# from random import *
# # import random
# # from random import randint as gimme_something, choice


# def random():
#     print("Hello World")


# # Modules
# print(random())
# print(randint())

# my_list = [1, 2, 435, 32, 324, 2, 32]

# print(something_cool(my_list))


# pip install NAME_OF_PACKAGE


# __name__ variable
# __name__ = __main__

# Object Oriented Programming (OOP)


# my_list = [1,2,3,4]
# my_dict = {"a": 23}


# class
# objects

# Dog  ##    <- class | blueprint
# name
# breed
# bark
# protect
# play fetch
# smell drug

## Dog ##
# name = Tommy
# breed = Mastiff
# bark
# protect

## Dog ##
# name = Motilal
# breed = Pug
# bark


# User ##    <- class | blueprint
# first_name
# last_name
# dob
# upload_profile_photo()
# add_post()
# upload_photos()

# Encapsulation & Abstraction

# User
# Amit
# Singh
# 12-6-1989

# upload_profile_photo
# add_post
# upload_photos


list1 = [1, 2, 3, 4]
list2 = [1, 2, 3, 4].append()

name = "Hello World"

amit = User("amit", "singh", "12-6-1989")
michael = User("michal", "jackson", "12-34-1966")


amit.add_post()


# Rummy <-

# Game
# Player = []
# Card
# Deck

# Bet
# Chip
# Hand
# Shuffle

# ---------------------
# id234324
# Player = ["user1", "amit"]
# Deck 52

# id234324.shuffle()
# id234324.hand()


list1.append(21)


first_name = "Amit"


def upload_profile_photo():
    # dsafdsaf


class User:
    first_name = ""

    def upload_profile_photo():
        # dsafdsaf


amit.upload_profile_photo()


amit = User("amit", "singh")
michael = User("michael", "jackson")


# Vehicle
# make
# model
# year
# color
# condition

# buy()
# display_vehicle

# Users -> who use our application
# Use Pascal casing to write the names of your classes

# snake_case
# snakeCaseCaseWord
# PascalCaseIsSomethingVeryImportant

# class User:
#     pass

# We create objects using this User class blueprint
# We are creating instances of this class


# # Attributes are attached to the objects that we create using this class.
# INSTANCE ATTRIBUTES

# class User:
#     # On creation of an object of this class, the __init__ method will run by default
#     # We always instantiate all the attributes
#     def __init__(self, first_name, last_name, age):
#         # Attributes are attached to the instances of this class
#         self.first_name = first_name
#         self.last_name = last_name
#         if age < 0:
#             raise ValueError(
#                 "How can someone's age be negative! Please add a proper value!")
#         else:
#             self.age = age


# # user1 = User("amit", "singh", 20)
# # print(user1.first_name, user1.last_name)

# first = input("Please enter a first name: ")
# second = input("Please enter a second name: ")
# age = int(input("Please enter an age: "))

# while age < 0:
#     age = int(input("Please enter a valid age! : "))

#     if age > 0:
#         break

# user1 = User(first, second, age)

# print(f"{user1.first_name} has been created!")


# # user2 = User()

# # print(user1.first_name, user1.last_name, user1.age)
# # print(user2.first_name)
# # michael = User()


# # print(amit)
# # print(type(amit))

# # encapsulate, abstraction


# # ----------------------------------------------
# # Public/Private attributes
# # _name
# # __name
# # __name__ -> dunder

# # Poker game
# class User:
#     def __init__(self, first_name, last_name, age, created_at):
#         self.first_name = first_name
#         self.last_name = last_name
#         self._age = age

#     def full_name(self):
#         return f"{self.first_name} {self.last_name}"

#     def is_senior(self):
#         if self._age >= 65:
#             return f"{self.first_name} is a senior citizen"
#         else:
#             return f"{self.first_name} is not a senior citizen"

#     def favorite_food(self, food):
#         return f"{self.first_name} love to eat {food}"


# class Poker:
#     def __init__(self, player1, player2):
#         self.player1 = player1
#         self.player2 = player2

#     def start_game(self):
#         return "Game has started!"


# user1 = User("alan", "turing", 30)
# user2 = User("mary", "shapiro", 75)

# dracula = User("Vlad", "Draculia", 1100)

# # User.__init__("Vlad", "Draculia", 1100)

# # print(user1._age)
# # print(user1.full_name())
# print(user1.is_senior())
# print(user2.is_senior())

# print(user2.favorite_food("Chips"))

# # game = Poker("Alan", "Mary")

# # print(game.full_name())


# # --------------------------------------

# # CLASS ATTRIBUTES

# class User:
#     total_users = 0
#     age_limit = 18

#     def __init__(self, first_name, last_name, age):
#         self.first_name = first_name
#         self.last_name = last_name
#         if age < User.age_limit:
#             raise ValueError("User has to be above 18")
#         else:
#             self._age = age
#         User.total_users += 1

#     def full_name(self):
#         return f"{self.first_name} {self.last_name}"

#     def is_senior(self):
#         if self._age >= 65:
#             return f"{self.first_name} is a senior citizen"
#         else:
#             return f"{self.first_name} is not a senior citizen"

#     def favorite_food(self, food):
#         return f"{self.first_name} love to eat {food}"


# print(User.total_users)
# user1 = User("Alan", "Turing", 30)
# print(User.total_users)
# user2 = User("Marie", "Curie", 21)
# print(User.total_users)


# # print(user1.first_name, user1.some_string)

# # print(user2.first_name, user2.some_string)


# ---------------------------------------------


# CLASS ATTRIBUTES

class User:
    total_users = 0
    age_limit = 18

    @classmethod
    def display_total_user(cls):
        return f"The total number of users are {cls.total_users}"

    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        if age < User.age_limit:
            raise ValueError("User has to be above 18")
        else:
            self._age = age
        User.total_users += 1

    # __repr__ function
    def __repr__(self):
        return f"------------\ntype:{type(self)}\nfirst_name:{self.first_name}\nlast_name:{self.last_name}\nage:{self._age}\n-------------"

    def full_name(self):
        return f"{self.first_name} {self.last_name}"

    def is_senior(self):
        if self._age >= 65:
            return f"{self.first_name} is a senior citizen"
        else:
            return f"{self.first_name} is not a senior citizen"

    def favorite_food(self, food):
        return f"{self.first_name} love to eat {food}"


# print(User.display_total_user())
user1 = User("Alan", "Turing", 30)
# print(User.display_total_user())
user2 = User("Marie", "Curie", 21)
# print(User.display_total_user())

print(user1)
print(user2)


######### HOME WORK ###########
# BANK ACCOUNT OOP EXERCISE

# Define a new class called - BankAccount.
# - Each BankAccount should have an owner, specified when a new BankAccount is created like BankAccount("Charlie")
# - Each BankAccount should have a `balance attribute` that always starts out as 0.0
# - Each instance should have a deposit method that accepts a number and adds it to the balance. It should return the new balance.
# - Each instance should have a withdraw method that accepts a number and subtracts it from the balance. It should return the new balance.
# PyPi - https://pypi.org/
# Use this site to search for external modules and libraries


# #### Inheritance

# Reddit, Forums, Facebook Groups => Users, Moderators and Admins.
# reduce the amount of code we write

# Reddit, Forums, Facebook Groups
# User
# attributes: first_name, last_name, username, password, email
# capabilities: comment(), add_post(), login(), logout()

# Moderator
# attributes: first_name, last_name, username, password, email, group
# capabilities: comment(), add_post(), login(), logout(), delete_posts(), delete_comments(), ban_users()

# Inheritance
# Create a relationship, between our classes.
# In python, when we create our class, we pass the parent class as an argument

# __init__ => constructor

# class Animal:
#     cool = True

#     def make_sound(self, sound):
#         print(f"This animal says {sound}")


# class Dog(Animal):
#     def favorite_toy(self, name):
#         print(f"Dog's favorite toy is {name}")


# tiger = Animal()
# tiger.make_sound("roar!")


# pooch = Dog()
# pooch.make_sound("Bow Wow!")
# print(pooch.cool)
# pooch.favorite_toy("Bone")

# print(isinstance(pooch, Animal))
# print(isinstance(pooch, object))

# object => all these classes, Animal, Dog, list, dict, tuple, sets, str, int. All these inherit from the base `object` class


# ##### Properties

# class Human:
#     def __init__(self, first, last, age):
#         self.first = first
#         self.last = last
#         self._age = age

#     # # Getter
#     # def get_age(self):
#     #     return self._age

#     # # Setter
#     # def set_age(self, value):
#     #     self._age = value

#     # define a property (getter)
#     @property
#     def age(self):
#         return self._age

#     # define a setter for our `age` property
#     @age.setter
#     def age(self, value):
#         self._age = value


# jack = Human("Jack", "Sheppard", 28)

# # print(jack.get_age())

# # jack.set_age(40)

# # print(jack.get_age())

# print(jack.age)

# jack.age = 40

# print(jack.age)

# jack._age = 1000

# print(jack.age)


# #### super()

# class Animal:
#     def __init__(self, name, species):
#         self.name = name
#         self.species = species

#     def __repr__(self):
#         return f"{self.name} is a {self.species}"

#     def make_sound(self, sound):
#         print(f"This animal says {sound}")


# class Dog(Animal):
#     def __init__(self, name, breed, toy):
#         # Animal.__init__(self, name, species="Canine")
#         super().__init__(name, species="Canine")
#         self.breed = breed
#         self.toy = toy


# tigre = Animal("tigre", "Feline")

# print(tigre)

# tommy = Dog("tommy", "pug", "bone")

# print(tommy)


# ##### Practical examples using all the above

# class User:
#     active_users = 0

#     @classmethod
#     def show_active_users(cls):
#         return f"There are currently {cls.active_users} active users"

#     def __init__(self, first, last, age):
#         self.first = first
#         self.last = last
#         self.age = age
#         User.active_users += 1

#     def full_name(self):
#         return f"{self.first} {self.last}"

#     def initials(self):
#         return f"{self.first[0]}{self.last[0]}"

#     def is_senior(self):
#         return self.age >= 65

#     def birthday(self):
#         self.age += 1
#         return f"Happy {self.age}th, {self.first}"

#     def logout(self):
#         User.active_users -= 1
#         return f"{self.first} has logged out!"


# class Moderator(User):
#     total_mods = 0

#     def __init__(self, first, last, age, group):
#         super().__init__(first, last, age)
#         self.group = group
#         Moderator.total_mods += 1

#     @classmethod
#     def show_active_mods(cls):
#         return f"There are currently {cls.total_mods} active mods"

#     def remove_post(self):
#         return f"{self.first} removed a post from the {self.group} group"

#     def logout(self):
#         User.active_users -= 1
#         Moderator.total_mods -= 1
#         return f"{self.first} has logged out!"


# amit = User("Amit", "Singh", 31)

# # print(amit.last)

# alan = Moderator("Alan", "Turing", 40, "Python Devs")
# jim = Moderator("Jim", "Jacobs", 30, "Django Devs")

# # print(alan.remove_post())
# print(amit.logout())
# print(User.show_active_users())

# print(Moderator.show_active_mods())

# print(jim.logout())
# print(Moderator.show_active_mods())

# print(alan.is_senior())


# #### Multiple Inheritance

# class Animal:
#     def __init__(self, name):
#         print("ANIMAL INIT")
#         self.name = name

#     # def walk(self):
#     #     return f"{self.name} is walking"

#     # def greet(self):
#     #     return f"I am {self.name} of the land!"


# class Fish:
#     def __init__(self, name):
#         print("FISH INIT")
#         self.name = name

#     def swim(self):
#         return f"{self.name} is swimming"

#     def greet(self):
#         return f"I am {self.name} of the sea!"


# class Amphibian(Animal, Fish):
#     def __init__(self, name):
#         print("AMPHINIAN INIT")
#         super().__init__(name)


# salamander = Amphibian("Salamander")

# print(salamander.greet())


# #### MRO - Method Resolution Order

# class A:
#     def do_something(self):
#         print("Method defined in A")


# class B(A):
#     pass
#     # def do_something(self):
#     #     print("Method defined in B")


# class C(A):
#     pass
#     # def do_something(self):
#     #     print("Method defined in C")


# class D(C, B):
#     pass


# thing = D()
# thing.do_something()


# # print(D.mro())
# print(help(D))  # To see Python's MRO hierarchy


# ###### Polymorphism
# 1. The same method in a class works in a similar way for different classes
# A common implementation of this style, is when we have a method in a parent/base class that is
# overriddent by a subclass. This is called `method overriding`

# class Animal:
#     def speak(self):
#         raise NotImplementedError("Subclass needs to impement this method")


# class Dog(Animal):
#     def speak(self):
#         return "bark!"


# class Cat(Animal):
#     def speak(self):
#         return "purr"


# class Fish(Animal):
#     pass


# d = Dog()
# print(d.speak())
# f = Fish()
# print(f.speak())


# ###### Polymorphism
# 2. The same method (operation) works (differently) different kinds of objects
# "special methods" <- dunder methods
# "magic methods"
# https://docs.python.org/3/reference/datamodel.html#special-method-names


# 8 + 2
# "8" + "2"

# __add__() => this methods get called on the first operand
# __len__()

# class Human:
#     def __init__(self, height):
#         self.height = height  # in inches

#     def __len__(self):
#         return int(self.height)

#     def __repr__(self):
#         return "Hello World"


# amit = Human(6)

# print(len(amit))
# print(amit)


# ##### Example 2 (USING OPERATORS SPECIAL METHODS)

# from copy import copy  # documentation copy


# class Human:
#     def __init__(self, first, last, age):
#         self.first = first
#         self.last = last
#         if type(age) != int:
#             raise ValueError("Please enter an interger for the age")
#         else:
#             self.age = age

#     def __repr__(self):
#         return f"Human named {self.first} {self.last} aged {self.age}"

#     def __len__(self):
#         return self.age

#     def __add__(self, other):
#         if isinstance(other, Human):
#             return Human(first="Newborn", last=self.last, age=0)
#         else:
#             raise TypeError(
#                 "Humans can only mate with Humans!")

#     def __mul__(self, other):
#         if isinstance(other, int):
#             return [copy(self) for i in range(other)]
#         else:
#             raise TypeError("Can't multipy")


# amit = Human("amit", "singh", 30)
# # priya = Human("priya", "shah", 25)

# # print(amit.last)
# # print(priya.last)

# # baby = amit + priya
# # print(baby.first)

# cloned = amit * 3


# cloned[1].first = "alan"

# print(cloned[0])
# print(cloned[1])
# print(cloned[2])


# #### Iterators

# Iterators
# next() on it

# Iterables
# iter() on it

# name = "Albert"

# def my_for(iterable, func):
#     if (iter(iterable)):
#         iterator = iter(iterable)
#     else:
#         return "ERROR"

#     while True:
#         try:
#             i = next(iterator)
#         except StopIteration:
#             break
#         else:
#             print(i)


# # Recreated our own for loop
# my_for([1, 2, 3, 4], print)


# for x in [1, 2, 3, 4]:
#     print(x)


# # #### Custom Iterable Class/Object, Counter(0, 10)

# class Counter:
#     def __init__(self, start, end, step):
#         self.start = start
#         self.end = end
#         self.step = step

#     def __iter__(self):
#         return self

#     def __next__(self):
#         if self.start < self.end:
#             num = self.start
#             self.start += self.step
#             return num
#         else:
#             raise StopIteration


# count = Counter(0, 10, 2)

# for x in count:
#     print(x)


# ##### Poker - iterable deck

from random import shuffle


class Card:
    def __init__(self, suit, value):
        self.suit = suit
        self.value = value

    def __repr__(self):
        return f"{self.value} of {self.suit}"


class Deck:
    def __init__(self):
        suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
        values = ['A', '2', '3', '4', '5', '6',
                  '7', '8', '9', '10', 'J', 'Q', 'K']
        self.cards = [Card(suit, value) for suit in suits for value in values]

    def __repr__(self):
        return f"Deck of {self.count()} cards."

    def __iter__(self):
        return iter(self.cards)

    def reset(self):
        suits = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
        values = ['A', '2', '3', '4', '5', '6',
                  '7', '8', '9', '10', 'J', 'Q', 'K']
        self.cards = [Card(suit, value) for suit in suits for value in values]
        return self

    def count(self):
        return len(self.cards)

    def _deal(self, num):
        """
        Return a list of cards dealt
        """
        count = self.count()
        actual = min([num, count])  # make sure we don't try to over-deal

        if count == 0:
            raise ValueError("All cards have been dealt")

        if actual == 1:
            return [self.cards.pop()]

        cards = self.cards[-actual:]  # slice off the end
        self.cards = self.cards[:-actual]  # adjust cards

        return cards

    def shuffle(self):
        if self.count() < 52:
            raise ValueError("Only full decks can be shuffled")

        shuffle(self.cards)
        return self

    def deal_card(self):
        """
        Returns a single Card
        """
        return self._deal(1)[0]

    def deal_hand(self, hand_size):
        """
        Returns a list of Cards
        """
        return self._deal(hand_size)


my_deck = Deck()


for card in my_deck:
    print(card)
# Generators

# We can create generators in 2 ways:
# 1. Generators can be created using `generator functions` - (yield)
# 2. Generators can be created with `generator expressions`


# ## Generator Functions

# def count_up_to(max):
#     count = 1
#     while count <= 10:
#         yield count
#         count += 1

# def count_from(start, end):
#     count = start
#     while count <= end:
#         yield count
#         count += 1


# counter = count_from(0, 10)


# lst = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# for num in counter:
#     print(num)

# for num in lst:
#     print(num)

# print(count_from(0, 10))

# help(counter)
# print(next(counter))
# print(next(counter))
# print(next(counter))

# for num in counter:
#     print(num)


# def count_from(start, end):
#     count = start
#     while count <= end:
#         return count
#         count += 1


# def count_from(start, end):
#     count = start
#     while count <= end:
#         yield count
#         count += 1


# counter = count_from(1, 3)

# [1, 2, 3]

# print(next(counter)) # 1
# print(next(counter)) # 2
# print(next(counter)) # 3
# print(next(counter))

# counter = 1
# for num in counter:
#     print(num)


# 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4
# measure


# def make_beat_list():
#     max = 100
#     nums = (1, 2, 3, 4)
#     i = 0
#     result = []

#     while len(result) < max:
#         if i >= len(nums):
#             i = 0
#         result.append(nums[i])
#         i += 1

#     return result


# def make_beat_gen():
#     nums = (1, 2, 3, 4)
#     i = 0
#     while True:
#         if i >= len(nums):
#             i = 0
#         yield nums[i]
#         i += 1


# music_list = make_beat_list()  # [1,2,3,4,5,6,7,8]
# music_gen = make_beat_gen() # [1]


# for num in music_list:
#     print(num)

# for num in music_gen:
#     print(num)

# [1, 1, 2, 4]

# # EXAMPLE

# def fib_list(max):
#     nums = []
#     a, b = 0, 1

#     while len(nums) < max:
#         nums.append(b)
#         a, b = b, a+b

#     return nums


# def fib_gen(max):
#     x = 0
#     y = 1
#     count = 0

#     while count < max:
#         x, y = y, x+y
#         yield x
#         count += 1


# # for num in fib_list(1000000):
# #     print(num)

# for num in fib_gen(100000000):
#     print(num)


# # Generator expressions

# from time import time

# gen_start_time = time()
# # (num for num in range(1000000))            # <genObj>
# print(sum((num for num in range(10000000))))  # [1,2,3,4,5,6,7,8,9,10]
# gen_stop_time = time()

# gen_result = gen_stop_time - gen_start_time

# list_start_time = time()
# # [num for num in range(1000000)]
# print(sum([num for num in range(10000000)]))
# list_stop_time = time()

# list_result = list_stop_time - list_start_time

# print(f"Generator total time: {gen_result}")
# print(f"List total time: {list_result}")


# ### Decorators

# Higher Order Functions
# A higher order function is just a function that either return another function
# or accepts functions as argument


# def sum(n, func):
#     total = 0
#     for num in range(1, n+1):
#         total += func(num)
#     return total


# def square(x):
#     return x*x


# def cube(x):
#     return x*x*x


# print(sum(10, square))
# print(sum(10, cube))


# Nesting function inside of another function

from random import choice


# def greet(name):
#     def mood():
#         message = choice(("Hello ", "Get lost ", "I like you "))
#         return message

#     result = mood() + name
#     return result


# print(greet("Amit"))


# Return functions


# # CLOSURE
# def use_slang(name):
#     def get_slang():
#         word = choice(('Yo', 'Wasssup', 'Bruv', 'Bantai'))
#         return f"{word} {name}"

#     return get_slang


# say_hi = use_slang("Amit")

# print(say_hi())


# DECORATOR - syntax
# they are functions


# def be_polite(fn):
#     def wrapper():
#         print("It was really good talking to you!")
#         fn()
#         print("See you!")

#     return wrapper


# @be_polite  # greet = be_polite(greet)
# def greet():
#     print("My name is Alan")


# @be_polite
# def anime():
#     print("I really like Death Note")


# # greet = be_polite(greet)
# # greet()


# greet()
# anime()


# ###

def say(fn):
    def wrapper(*args, **kwargs):
        return fn(*args, **kwargs).upper()
    return wrapper


@say
def greet(name):
    return f"Hi, I'm {name}"


@say
def greet2(name1, name2):
    return f"{name1} is the best friend of {name2}"


# my_func = say(greet)

# my_func(name)

# print(greet("Alan"))

print(greet2("Alan", "Lisa"))
# Decorators Example

# HOF. They accept functions as arguments and they return back function

# from functools import wraps


# def introduction(fn):
#     @wraps(fn)
#     def wrapper(*args, **kwargs):
#         """I am the wrapper function!!!"""
#         print("Hello Everyone")
#         fn(*args, **kwargs)
#         print("I am going to be your trainer!")
#     return wrapper


# @introduction
# def instructor(name):
#     """Introduce the instructor"""
#     print(f"My name is {name}")


# @introduction
# def trainer(name, course):
#     """Trainer's name and his course"""
#     print(f"My name is {name}, and I will teach you {course}")


# # instructor("Rahul")
# # trainer("Edgar", "Physics")

# # trainer = introduction(trainer)


# # Standard format -> decorator pattern

# # def my_decorator(fn):
# #     def wrapper(*args, **kwargs):
# #         # do some stuff with fn(*args, **kwargs)
# #     return wrapper


# # print(trainer.__doc__)
# # print(trainer.__name__)
# print(help(trainer))


# from functools import wraps
# from time import time


# def speed_test(fn):
#     @wraps(fn)
#     def wrapper(*args, **kwargs):
#         start_time = time()
#         result = fn(*args, **kwargs)
#         end_time = time()
#         print(f"Executing {fn.__name__}")
#         print(f"Time Elapsed: {end_time - start_time}")
#         return result
#     return wrapper


# @speed_test
# def sum_nums_gen():
#     return sum(x for x in range(10000000))


# @speed_test
# def sum_nums_list():
#     return sum([x for x in range(10000000)])


# print(sum_nums_gen())
# print(sum_nums_list())


# Enforce restrictions on arguments - example
# boilerplate

# from functools import wraps


# # Argument Guard
# def ensure_no_kwargs(fn):
#     @wraps(fn)
#     def wrapper(*args, **kwargs):
#         if kwargs:
#             raise ValueError("No kwargs allowed!")
#         else:
#             return fn(*args, **kwargs)
#     return wrapper


# @ensure_no_kwargs
# def greet(name):
#     print(f"Hi there, {name}")


# # greet = ensure_no_kwargs(greet)


# greet("Amit")


# #### Argument checking Guard

from functools import wraps


def ensure_first_arg_is(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        if args and args[0] != "vada pav":
            return f"First arg should be vada pav"
        else:
            return fn(*args, **kwargs)
    return wrapper


# def ensure_first_arg_is(val):
#     def inner(fn):
#         @wraps(fn)
#         def wrapper(*args, **kwargs):
#             if args and args[0] != val:
#                 return f"First arg should be {val}"
#             else:
#                 return fn(*args, **kwargs)
#         return wrapper
#     return inner


# @ensure_first_arg_is("burger")
# def fav_foods(*foods):
#     print(foods)


# @ensure_first_arg_is(10)
# def add_to_ten(num1, num2):
#     return num1 + num2


# # Logic.. evaluation
# # fav_foods = ensure_first_arg_is("vada pav")(fav_foods)
# # fav_foods = inner(fav_foods)
# # fav_foods = wrapper
# # fav_foods("vada pav")


# # requirement
# print(fav_foods("burger", "french fries", "vada pav"))
# print(add_to_ten(10, 12))


# # ####

# def enforce(*types):
#     def decorator(fn):
#         def wrapper(*args, **kwargs):
#             newargs = []
#             for (a, t) in zip((args), types):
#                 newargs.append(t(a))
#             return fn(*newargs, **kwargs)
#         return wrapper
#     return decorator


# @enforce(str, int)
# def repeat_msg(msg, times):
#     for time in range(times):
#         print(msg)


# message = input("What is your message: ")
# num = input("Number of times you want to print this message: ")

# repeat_msg(message, num)


# ###### TESTING
# Test-Driven Development (TDD)

# 1. assert statement
# 2. doctests
# 3. unit testing


# Test-Driven Development (TDD)
# Red, Green, Refactor


# Assertions

# A statement in python
# we use the `assert` statement
# If our tests results in False, `AssertionError` will be raise


# def add_positive_nums(x, y):
#     assert x > 0 and y > 0, "Both numbers must be positive!"
#     return x + y


# print(add_positive_nums(-1, 2))


# def eat_veg(food):
#     assert food in ["broccoli", "cauliflower", "carrots",
#                     "onions"], "Food must not be non-vegetarian"
#     return f"I am eating {food}"


# print(eat_veg("chicken"))

# TO BYPASS ASSERTS, run the file with the -O flag
# ex. python -O FILE_NAME.py


# #### doctests
# You add your doctests... inside your """ """
# syntax is a major drawback....


# def add(x, y):
#     """
#     >>> add(1,3)
#     4
#     """
#     return x + y

# Execute your file using this command
# python -m doctest -v YOUR_FILE_NAME.py


def double(values):
    """
    >>> double([1,2,3,4])
    [2, 4, 6, 8]

    >>> double([])
    []

    >>> double(['a','b','c'])
    ['aa', 'bb', 'cc']

    >>> double([True, None])
    Traceback (most recent call last):
        ...
    TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'
    """
    return [item*2 for item in values]


help(double)
# Unit Tests

# unittests <- module
# Assertions

#  https://docs.python.org/3/library/unittest.html

# Web Application

# TEST1, TEST2, TEST3, TEST4
# FUNC => setting up the database
# FUNC => destroy the database


# FUNC => setting up the database
# Test1
# FUNC => destroy the database
# FUNC => setting up the database
# TEST2
# FUNC => destroy the database
# FUNC => setting up the database
# TEST3
# FUNC => destroy the database
# TEST4


# Before and After hooks
# setUp() -> runs before every test
# tearDown() -> runs after every test

# To get verbose output use -v flag
# python NAME_OF_TEST_FILE.py -v

# ----------------------------------

# File (Input Output)
# Read from files, write to files

# JSON
# CSV
# with statement

# Reading files
# open function

# file = open("data.txt")
# print(file)


# We can move the python cursor using `seek` method


# >>>
# >>> file = open("data.txt", "r")
# >>> file
# <_io.TextIOWrapper name='data.txt' mode='r' encoding='cp1252'>
# >>> file.read()
# 'I am some data'
# >>> file.read()
# '\nI am some more data\nI am another line of text data'
# >>> file.read()
# ''
# >>> file.read()
# '\nSome other line'
# >>> file.seek(0)
# 0
# >>> file.read()
# 'I am some data\nI am some more data\nI am another line of text data\nSome other line'
# >>> file.seek(30)
# 30
# >>> file.read()
# ' data\nI am another line of text data\nSome other line'
# >>> file.seek(0)
# 0
# >>> file.readline()
# 'I am some data\n'
# >>> file.seek(0)
# 0
# >>> file.read()
# 'I am some data\nI am some more data\nI am another line of text data\nSome other line'
# >>> file.seek(0)
# 0
# >>> file.readline()
# 'I am some data\n'
# >>> file.readline()
# 'I am some more data\n'
# >>> file.readline()
# 'I am another line of text data\n'
# >>> file.readline()
# 'Some other line'
# >>> file.readline()
# ''
# >>> file.seek(0)
# 0
# >>> file.readlines()
# ['I am some data\n', 'I am some more data\n', 'I am another line of text data\n', 'Some other line']
# >>>
# >>>
# >>> file.closed
# False
# >>> file.close()
# >>> file.closed
# True
# >>>
# >>>
# >>> file.read()
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# ValueError: I/O operation on closed file.
# >>>


# ALTERNATIVE SYNTAX FOR WORKING WITH FILES (IO)
# with blocks

# # Option 1
# file = open("data.txt")
# file.read()
# file.seek(0)
# file.readline()
# file.close()


# # Option 2
# with open("data.txt") as file:
#     file.read()
#     print(file.closed)

# print(file.closed)

# # similar to for loops, how they use the __iter__() and the __next__()
# # `with` statements use the dunders, __enter__() and __exit__(). __exit__() is
# # automatically called when the with state is done.


# Writing Files (.write() method)
# We still have to first open the file
# Only then can we write to a file

# specify "w" flag on the open function
# file = open("data.txt", "r") # by default "r" is the mode

# with open("data.txt", "w") as file:
#     file.write("THIS IS SOME NEWLY WRITTEN TEXT\n")
#     file.write("Hello World\n")

# "w" delete everything currently in the file.. and then write the new things..
# replace all the text inside of it

# with open("lol.txt", "w") as file:
#     file.write("haha" * 10000)


# If we want to preserve the existing content inside a file
# and just add new content to it
# USe the "a" flag (append)

# with open("data.txt", "a") as file:
#     file.write("This text has just been appended to this file\n")
#     file.write("This is some more text\n")

# # NOTE: Add a \n - newline character in the end if you want the next .write() method
# # to be printed on the next line


# with open("data.txt", "a") as file:
#     file.seek(0)  # THIS WILL NOT WORK
#     file.write("MY FILE - data.txt\n")

# # "a" append does not allow us to print on some other position

# # To write based on cursor
# # "r+"
# with open("data2.txt", "r+") as file:
#     file.write("<REPLACED>")
#     file.seek(200)
#     file.write("<REPLACED>")

# # NOTE:- "r+" will not create a file.. if it doesn't already exist.


# def copy(file_name, new_file_name):
#     with open(file_name) as file:
#         data = file.read()

#     with open(new_file_name, "w") as file:
#         file.write(data[::-1])


# copy("data.txt", "data2.txt")

# Exercise:
# create a function, where you can copy from 2 files and then create new file with the
# with the contents of the 2 files


# CSV - Reading, write to CSV files

# Reading CSV

# with open("people.csv") as file:
#     lines = file.readlines()
#     print(lines[3].split(","))

# CSV Module
# Two ways of reading into a CSV
# reader
# DictReader

# reader function
# lets us iterate over rows of the CSV as lists... each row is a separate list

# reader() function converts each line to a list


# from csv import reader, DictReader

# with open("people.csv") as file:
#     csv_reader = reader(file)
#     # print(data)
#     # print(csv_reader)

#     # next(csv_reader)  # to get rid of the header
#     for person in csv_reader:
#         print(person)
#         # print(f"{person[0]} is from {person[1]}")


# with open("people.csv") as file:
#     csv_reader = DictReader(file)
#     # print(csv_reader)

#     for row in csv_reader:
#         print(row)
#         # print(row["Name"])


# with open("people2.csv") as file:
#     csv_reader = reader(file, delimiter="$")

#     for row in csv_reader:
#         print(row)


# Write CSV Files
# Again there 2 methods

# Using lists
# writer
# writerow

# from csv import writer
# # pass newline="" (empty string) to avoid extra newlines in file (WINDOWS only)
# with open("test_writing.csv", "w", newline="") as file:
#     csv_writer = writer(file)
#     csv_writer.writerow(["Name", "Age"])
#     csv_writer.writerow(["Alan", "30"])
#     csv_writer.writerow(["Mary", "28"])
#     csv_writer.writerow(["30", "Jake"])


# from csv import reader, writer

# # Method 1
# with open("people2.csv") as file:
#     csv_reader = reader(file)
#     data = [[line.upper() for line in row] for row in csv_reader]

# with open("people3.csv", "w", newline="") as file:
#     csv_writer = writer(file)

#     for people in data:
#         csv_writer.writerow(people)


# # Method 2
# with open("people2.csv") as file:
#     csv_reader = reader(file)

#     with open("people3.csv", "w", newline="") as file:
#         csv_writer = writer(file)

#         for people in csv_reader:
#             csv_writer.writerow([line.upper() for line in people])


# Writing to CSVs using Dictionaries

# - DictWriter

# - fieldnames - kwargs for the DictWriter specifying headers
# - writeheader - to write the header row
# - writerow - write the row based on a dictionary

# from csv import DictWriter

# with open("testing_writing.csv", "w", newline="") as file:
#     headers = ["Name", "Breed", "Age"]
#     csv_writer = DictWriter(file, fieldnames=headers)
#     csv_writer.writeheader()
#     csv_writer.writerow({
#         "Name": "Tommy",
#         "Age": 3,
#         "Breed": "German Shepard"
#     })
#     csv_writer.writerow({
#         "Name": "Trigon",
#         "Age": 5,
#         "Breed": "Bulldog"
#     })


# Pickling

import pickle


class Animal:
    def __init__(self, name, species):
        self.name = name
        self.species = species

    def __repr__(self):
        return f"{self.name} is a {self.species}"

    def make_sound(self, sound):
        print(f"This animal says {sound}")


class Dog(Animal):
    def __init__(self, name, breed, toy):
        super().__init__(name, species="Dog")
        self.breed = breed
        self.toy = toy

    def play(self):
        print(f"{self.name} plays with {self.toy}")


# tommy = Dog("Tommy", "Pug", "Bone")
# dexter = Dog("Dexter", "German Shepard", "Carpet")

# # "wb" flag - binary
# with open("dogs.pickle", "wb") as file:
    # # pickle.dump(tommy, file)
    # pickle.dump((tommy, dexter), file)

# with open("dogs.pickle", "rb") as file:
#     # restored_tommy = pickle.load(file)
#     restored_tommy, restored_dexter = pickle.load(file)

#     print(restored_tommy)
#     print(restored_dexter)


# JSON
# SCRATCHPAD #15 -> JSON Pickling

# XML
# JSON - JavaScript Object Notation

# import json

# data = ["alan", 23, {"job": "trainer", "info": ("hello", "world")}]

# result = json.dumps(["alan", 23, {"job": "trainer", "info": ("hello", "world")}])

# # print(result)

# import jsonpickle


# class Dog:
#     def __init__(self, name, breed):
#         self.name = name
#         self.breed = breed

#     def bark(self):
#         print(f"{self.name} says Bark Bark Bark")


# # tommy = Dog("Tommy", "German Shepard")

# # # Write `tommy` object information to a json file
# # with open("dogs.json", "w") as file:
# #     saved_dog = jsonpickle.encode(tommy)
# #     file.write(saved_dog)


# with open("dogs.json", "r") as file:
#     contents = file.read()
#     # print(type(contents))
#     # contents = '{"py/object": "__main__.Dog", "name": "Tommy", "breed": "German Shepard"}'
#     restored_dog = jsonpickle.decode(contents)
#     # print(restored_dog)

# print(restored_dog.breed)
# print(restored_dog.bark())


# # print(tommy.__dict__)
# # print(saved_dog)

# # # Converted JSON
# # {
# #   "py/object": "__main__.Dog",
# #   "name": "Tommy",
# #   "breed": "German Shepard"
# # }


# # install jsonpickle module
# # python -m pip install jsonpickle

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Alan's great HTML Escape!</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <!-- This is a comment -->
    <h1 id="my_first_heading">Alan's Great Web Page</h1>
    <h2>Alan's Great Web Page</h2>
    <h3 id="my_second_heading">Alan's Great Web Page</h3>
    <h4>Alan's Great Web Page</h4>
    <h5>Alan's Great Web Page</h5>
    <h6>Alan's Great Web Page</h6>
    <img src="images/code.jpg" alt="Some piece of code" />
    <p class="my-para">
      This is a paragraph tag. This is a tag where I can add long form and short
      form content.
    </p>
    <p class="my-para">
      Beautiful Soup is a Python library for pulling data out of HTML and XML
      files. It works with your favorite parser to provide idiomatic ways of
      navigating, searching, and modifying the parse tree. It commonly saves
      programmers hours or days of work.
    </p>
    <div class="my-box">
      <div>
        <h3>My special section</h3>
        <h3>My other title</h3>
        <div>
          <h3>My special title</h3>
        </div>
      </div>
      <p class="my-para">
        These <strong>instructions</strong>
        <em>illustrate all major features</em> of Beautiful Soup 4, with
        examples. I show you what the library is good for, how it works, how to
        use it, how to make it do what you want, and what to do when it violates
        your expectations. This document covers Beautiful Soup version 4.9.1.
        The examples in this documentation should work the same way in Python
        2.7 and Python 3.2.
      </p>
    </div>
    <hr />
    <!-- Horizontal Rule -->
    <hr />
    <hr />

    <div class="special">
      <div class="special-child-1">
        <h2>My special-1 box</h2>
        <p>
          I show you what the library is good for, how it works, how to use it,
          how to make it do what you want, and what to do when it violates your
          expectations. This document covers Beautiful Soup version 4.9.1. The
          examples in this documentation should work the same way in Python 2.7
          and Python 3.2.
        </p>
      </div>
      <div class="special-child-2">
        <h2>My special-2 box</h2>
        <p>
          Beautiful Soup is a Python library for pulling data out of HTML and
          XML files. It works with your favorite parser to provide idiomatic
          ways of navigating, searching, and modifying the parse tree. It
          commonly saves programmers hours or days of work.
        </p>
      </div>
    </div>

    <div>
      <ul>
        <li>Point 1</li>
        <li>Point 2</li>
        <ul>
          <li>Sub point 1</li>
          <li>Sub point 2</li>
          <li>Sub point 3</li>
        </ul>
        <li>This is another bullet point</li>
      </ul>
      <ol>
        <li>Point 1</li>
        <li>Point 2</li>
        <li>This is another bullet point</li>
      </ol>
    </div>
  </body>
</html>
/* Cascading Style Sheets */

.my-para {
  font-size: 30px;
  color: red;
}

.my-box {
  background-color: lightyellow;
  padding: 100px;
  font-size: 30px;
}

.special {
  background-color: beige;
  padding: 70px;
  display: flex;
}

.special div {
  width: 50%;
  padding: 20px;
  background-color: red;
  color: white;
}

.special-child-1 {
  background-color: blue !important;
}

#my_first_heading {
  font-family: 'Arial', sans-serif;
  font-weight: 800;
  color: brown;
}

div {
  border: 2px solid black;
  margin: 10px;
}

h1 {
  text-decoration: underline;
}

body {
  font-family: 'Arial', sans-serif;
  padding-top: 50px;
  padding-bottom: 50px;
  padding-left: 100px;
  padding-right: 100px;
}
# # Web Scraping
# Beautiful Soup

# - `Requests` module will be used to download the html
# - We will provide this html to `BeautifulSoup`
# - `BeautifulSoup` lets us navigate through the HTML using python
# - `BeautifulSoup` does not download the HTML

# Install beautifulSoup
# python -m pip install bs4

from bs4 import BeautifulSoup

myhtml = """
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>First HTML Page</title>
  </head>
  <body>
    <div id="first">
      <h3 class="special" data-example="yes">Hello World!</h3>
      <p>Hello world and this is some more text...</p>
      <p>This is another paragraph</p>
      <p>This is the last one</p>
    </div>
    <ul>
      <li id="second" class="special">This is a special list item #1</li>
      <li class="special">This is another special list item #2</li>
      <li id="second">This is not a special list item</li>
    </ul>
    <div>BYE world</div>
    <div data-example="yes">BYE world Again</div>
  </body>
</html>
"""

# Parsing and navigation HTML
soup = BeautifulSoup(myhtml, "html.parser")

# print(type(myhtml))
# print(type(soup))

# print(soup.body)
# print(soup.body.div)

# Methods
# print(soup.find('div'))

# my_div = soup.find("div")  # return the very first div tag that it finds

# print(type(my_div))

# FIND ELEMENTS BY TAGNAME
# my_divs = soup.find_all("div")  # return a list of all the <elements>
# print(my_divs)

# my_ps = soup.find_all("p")
# print(my_ps)

# my_lists = soup.find_all('li')
# print(my_lists)

# FIND ELEMENTS BY ATTRIBUTES

# # some_class = soup.find_all(class_="special") # Find element by class attribute
# some_class = soup.find(class_="special")
# print(some_class)

# some_id = soup.find(id="first")  # Find element by id attribute
# print(some_id)


# Find element by data (or any attributes) attribute
# # some_attr = soup.find_all(attrs={"data-example": "yes"})
# some_attr = soup.find_all(attrs={"name": "viewport"})
# print(some_attr)


# ------------------------------ CSS Selectors ---------------------- #

# We use the .select() method to find elements

# Selector List
# - id:         #test
# - class:      .test

# my_id = soup.find_all(id="second")[0]
# my_id2 = soup.select("#second")[0]  # Select id... first element
# print(my_id)
# print(my_id2)

# my_cls = soup.select(".special")
# print(my_cls)

# for el in my_cls:
#     print(el)

# my_tags = soup.select("h3")[0]
# print(my_tags)


my_atr = soup.select("[data-example]")  # select data attributes CSS style
print(my_atr)
# Accessing Data

from bs4 import BeautifulSoup

myhtml = """
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>First HTML Page</title>
  </head>
  <body>
    <div id="first">
      <h3 class="super-special highlight-text" data-example="yes">Hello World!</h3>
      <p>Hello world and this is some more text...</p>
      <p>This is another paragraph</p>
      <p>This is the last one</p>
    </div>
    <ul>
      <li id="third" class="special">This is a special list item #1</li>
      <li class="special">This is another special list item #2</li>
      <li id="second">This is not a special list item</li>
      <li id="fourth" class="highlighted">This is not a special list item</li>
      <li id="something-else">This is not a special list item</li>
    </ul>
    <div>BYE world</div>
    <div data-example="yes">BYE world Again</div>
  </body>
</html>
"""

# Parsing and navigation HTML
soup = BeautifulSoup(myhtml, "html.parser")

# # el = soup.select(".special")[0]
# el = soup.select(".special")
# print(el)
# # print(el.get_text())


# for element in soup.select(".special"):
#     # # Get the inner text of the tag
#     # print(element.get_text())

#     # # Get the name of the tag on which this .special class exists
#     # print(element.name)

#     # print(f"{element.name} - {element.get_text()}")

#     # To access the attributes of that tag-html-object
#     # print(element.attrs)
#     print(element.attrs["class"]) # to get a list of all the class names (attribute)
#     print(element.attrs["class"][0]) # to access only the first item from the returned list


# # attr = soup.find(class_="special") # Search by the class attribute
# # attr = soup.find(id="first") # Search by the id attribute
# # attr = soup.find_all("h3") # Search by tagname

# attr = soup.find("h3").attrs["data-example"] # long cut
# attr = soup.find("h3")["data-example"] # Short cut

# attr = soup.find("h3")["class"] # Short cut

# attr = soup.find("h3").attrs["class"]

# # attr = soup.find("h3") # Short cut
# # print(attr["class"])


# Navigate
# Two ways of navigation out html
# Via tags - (attributes/properties)


# # data = soup.body # gives us the entire body tag and everything inside of it
# data = soup.body.contents # return a list with all the html tags/objects
# data = soup.body.contents[1] # returns the item at index 1
# data = soup.body.contents[1].contents
# data = soup.body.contents[1].contents[1]
# data = soup.body.contents[1].contents[1].get_text()
# print(data)


# data = soup.body.contents[1].contents[1].next_sibling # get the next sibling
# data = soup.body.contents[1].contents[1].next_sibling.next_sibling # get the next sibling
# print(data)


# data = soup.find(class_="super-special")
# data = soup.find(class_="super-special").parent # returns the parent element
# data = soup.find(class_="super-special").parent.parent
# data = soup.find(class_="super-special").parent.parent.parent
# print(data)


# ##########3 Navigating using methods

# data = soup.find(id="first")
# data = soup.find(id="first").next_sibling # using the `next_sibling` property/attribute
# data = soup.find(id="first").find_next_sibling()
# data = soup.find_all(class_="special")[1].find_previous_sibling()
# print(data)

# data = soup.select("[data-example]")
# data = soup.select("[data-example]")[1].find_previous_sibling()
# print(data)

# data = soup.find(id="third").find_next_sibling(id="second") # filter next sibling
# data = soup.find(id="third").find_next_sibling(class_="highlighted") # filter next sibling
# print(data)


# data = soup.find(id="third").find_parent() # find parent of the element
# data = soup.find(id="third").find_parent("body") # filter parent of the element
# data = soup.find(id="third").find_parent("html") # filter parent of the element
# print(data)

# help(data)
# Scraping ARS TECHNICA

import requests
from bs4 import BeautifulSoup
from csv import writer

response = requests.get("https://arstechnica.com/")
soup = BeautifulSoup(response.text, "html.parser")
articles = soup.find_all(class_="article")

with open("news_articles.csv", "w", newline="") as csv_file:
    csv_writer = writer(csv_file)

    # Write the headers
    csv_writer.writerow(["title", "author", "date",  "link"])

    for article in articles:
        title = article.find("h2").get_text()
        url = article.find("a")["href"]
        date = article.find("time").get_text()
        author = article.find("span").get_text()

        csv_writer.writerow([title, author, date, url])

Scratchpad #17 and project files

# PART 1

# Target website: http://quotes.toscrape.com/
# Objective: Build a guessing game

# Task 1: Write the scraping logic

import requests
from bs4 import BeautifulSoup

all_quotes = []
base_url = "http://quotes.toscrape.com"
url = "/page/1/"

while url:
    res = requests.get(f"{base_url}{url}")
    print(f"Now scraping {base_url}{url}")
    soup = BeautifulSoup(res.text, "html.parser")

    quotes = soup.find_all(class_="quote")
    # print(quotes[2])

    for quote in quotes:
        all_quotes.append(
            {
                "text": quote.find(class_="text").get_text(),
                "author": quote.find(class_="author").get_text(),
                "bio-link": quote.find("a")["href"]
            }
        )

    next_btn = soup.find(class_="next")
    # # print(next_btn)

    # if next_btn:
    #     url = next_btn.find("a")["href"]
    # else:
    #     url = None

    # shortcut to writing if
    url = next_btn.find("a")["href"] if next_btn else None


print(all_quotes)
# Target website: http://quotes.toscrape.com/
# Objective: Build a guessing game

# Task 1: Write the scraping logic
# Task 2: Randomly show user one quote and ask user to guess it's author

import requests
from bs4 import BeautifulSoup
from time import sleep
from random import choice

all_quotes = []
base_url = "http://quotes.toscrape.com"
url = "/page/1/"

while url:
    res = requests.get(f"{base_url}{url}")
    print(f"Now scraping {base_url}{url}")
    soup = BeautifulSoup(res.text, "html.parser")
    quotes = soup.find_all(class_="quote")

    for quote in quotes:
        all_quotes.append(
            {
                "text": quote.find(class_="text").get_text(),
                "author": quote.find(class_="author").get_text(),
                "bio-link": quote.find("a")["href"]
            }
        )

    next_btn = soup.find(class_="next")
    url = next_btn.find("a")["href"] if next_btn else None
    # sleep(2) # delay


quote = choice(all_quotes)
remaining_guesses = 4

print("Here's a quote: ")
print(quote["text"])
print(quote["author"])

guess = ""
while guess.lower() != quote["author"].lower() and remaining_guesses > 0:
    guess = input(
        f"Who said this quote? Guesses remaining: {remaining_guesses}\n")
    remaining_guesses -= 1

    if guess.lower() == quote["author"].lower():
        print("You are correct!")
        break

    if remaining_guesses == 3:
        res = requests.get(f"{base_url}{quote['bio-link']}")
        soup = BeautifulSoup(res.text, "html.parser")
        birth_date = soup.find(class_="author-born-date").get_text()
        birth_place = soup.find(class_="author-born-location").get_text()
        print(
            f"Here is a hint: The author was born on {birth_date} {birth_place}")
    elif remaining_guesses == 2:
        print(
            f"Here is another hint: The author's first name starts with a {quote['author'][0]}")
    elif remaining_guesses == 1:
        last_initial = quote['author'].split(' ')[1][0]
        print(
            f"Last hint: The author's last name starts with a {last_initial}")
    else:
        print(
            f"Sorry you ran out of guesses. The correct answer was {quote['author']}")
# PART 3

# Target website: http://quotes.toscrape.com/
# Objective: Build a guessing game

# Task 1: Write the scraping logic
# Task 2: Randomly show user one quote and ask user to guess it's author
# Task 3: Ask the user if he want to play again

# Shortcut for indentation:
# right -   ctrl + ]
# left  -   ctrl + [

import requests
from bs4 import BeautifulSoup
from time import sleep
from random import choice


BASE_URL = "http://quotes.toscrape.com"


# Scraping logic
def scrape_quotes():
    all_quotes = []
    url = "/page/1/"

    while url:
        res = requests.get(f"{BASE_URL}{url}")
        print(f"Now scraping {BASE_URL}{url}")
        soup = BeautifulSoup(res.text, "html.parser")
        quotes = soup.find_all(class_="quote")

        for quote in quotes:
            all_quotes.append(
                {
                    "text": quote.find(class_="text").get_text(),
                    "author": quote.find(class_="author").get_text(),
                    "bio-link": quote.find("a")["href"]
                }
            )

        next_btn = soup.find(class_="next")
        url = next_btn.find("a")["href"] if next_btn else None
        # sleep(2) # delay

    return all_quotes


# Game Logic - recursive function
def start_game(quotes):
    quote = choice(quotes)
    remaining_guesses = 4

    print("Here's a quote: ")
    print(quote["text"])
    print(quote["author"])

    guess = ""
    while guess.lower() != quote["author"].lower() and remaining_guesses > 0:
        guess = input(
            f"Who said this quote? Guesses remaining: {remaining_guesses}\n")
        remaining_guesses -= 1

        if guess.lower() == quote["author"].lower():
            print("You are correct!")
            break

        if remaining_guesses == 3:
            res = requests.get(f"{BASE_URL}{quote['bio-link']}")
            soup = BeautifulSoup(res.text, "html.parser")
            birth_date = soup.find(class_="author-born-date").get_text()
            birth_place = soup.find(class_="author-born-location").get_text()
            print(
                f"Here is a hint: The author was born on {birth_date} {birth_place}")
        elif remaining_guesses == 2:
            print(
                f"Here is another hint: The author's first name starts with a {quote['author'][0]}")
        elif remaining_guesses == 1:
            last_initial = quote['author'].split(' ')[1][0]
            print(
                f"Last hint: The author's last name starts with a {last_initial}")
        else:
            print(
                f"Sorry you ran out of guesses. The correct answer was {quote['author']}")

    again = ""
    while again.lower() not in ('y', 'yes', 'n', 'no'):
        again = input("Would you like to play again? (yes/no) ")

    if again.lower() in ('y', 'yes'):
        return start_game(quotes)
    else:
        print("Thank you for playing!")


if __name__ == "__main__":
    quotes = scrape_quotes()
    start_game(quotes)
# PART 4

# Target website: http://quotes.toscrape.com/
# Objective: Build a guessing game

# Task 1: Write the scraping logic
# Task 2: Randomly show user one quote and ask user to guess it's author
# Task 3: Ask the user if he want to play again
# Task 4: Refactor code

import requests
from bs4 import BeautifulSoup
from time import sleep
from random import choice


BASE_URL = "http://quotes.toscrape.com"


# Scraping logic
def scrape_quotes():
    all_quotes = []
    url = "/page/1/"

    while url:
        res = requests.get(f"{BASE_URL}{url}")
        print(f"Now scraping {BASE_URL}{url}")
        soup = BeautifulSoup(res.text, "html.parser")
        quotes = soup.find_all(class_="quote")

        for quote in quotes:
            all_quotes.append(
                {
                    "text": quote.find(class_="text").get_text(),
                    "author": quote.find(class_="author").get_text(),
                    "bio-link": quote.find("a")["href"]
                }
            )

        next_btn = soup.find(class_="next")
        url = next_btn.find("a")["href"] if next_btn else None
        # sleep(2) # delay

    return all_quotes


# Game Logic - recursive function
def start_game(quotes):
    quote = choice(quotes)
    remaining_guesses = 4

    print("Here's a quote: ")
    print(quote["text"])
    print(quote["author"])

    guess = ""
    while guess.lower() != quote["author"].lower() and remaining_guesses > 0:
        guess = input(
            f"Who said this quote? Guesses remaining: {remaining_guesses}\n")
        remaining_guesses -= 1

        if guess.lower() == quote["author"].lower():
            print("You are correct!")
            break

        if remaining_guesses == 3:
            res = requests.get(f"{BASE_URL}{quote['bio-link']}")
            soup = BeautifulSoup(res.text, "html.parser")
            birth_date = soup.find(class_="author-born-date").get_text()
            birth_place = soup.find(class_="author-born-location").get_text()
            print(
                f"Here is a hint: The author was born on {birth_date} {birth_place}")
        elif remaining_guesses == 2:
            print(
                f"Here is another hint: The author's first name starts with a {quote['author'][0]}")
        elif remaining_guesses == 1:
            last_initial = quote['author'].split(' ')[1][0]
            print(
                f"Last hint: The author's last name starts with a {last_initial}")
        else:
            print(
                f"Sorry you ran out of guesses. The correct answer was {quote['author']}")

    again = ""
    while again.lower() not in ('y', 'yes', 'n', 'no'):
        again = input("Would you like to play again? (yes/no) ")

    if again.lower() in ('y', 'yes'):
        return start_game(quotes)
    else:
        print("Thank you for playing!")


if __name__ == "__main__":
    quotes = scrape_quotes()
    start_game(quotes)
## EXERCISES 
### Exercise 1
> Your goal is to create and work with a file named `users.csv`. Each row of data consists of two columns: a user's first name and a user's last name. Create and implement a function called `add_user` which should accept a first name and a last name and then append a new user row to the `users.csv` file. We can then use the function anytime we want to add a new user to the `users.csv` file.


### Exercise 2
> Create a function called `print_users` which when called, it should print out the first and last names of all the users in the `users.csv` file.


### Exercise 3
> Write a function called `find_user` which should accept a first name and a last name and searches for a user with that first and last names in the `users.csv` file. If the user is found `find_user` should return the index where the user is found, otherwise it should return a message saying that the user wasn't found.<br />
> __HINT: use the enumerate() function to create index values__


### Exercise 4
> Write a function called `update_users` which should accept an *old first name*, an *old last name* and a *new first name* and a *new last name*. Search all the users with the old first and last name in the `users.csv` file and then update those names with the new ones. Lastly this function should return the count of how many users were updated.
```python
update_users("Alan", "Turing", "Babu", "Chapri") # Users updated: 2
```


### Exercise 5
> Write a function called `delete_users` which should accept a first and last name. Update the `users.csv` file so that any user whose first and last names match the inputs should be removed. In the end, return a count of how many users were removed.
```python
# Users deleted: 0
```









# Regular Expressions (REGEX)
# REGEX is just a way of describing patterns within text strings.
# REGEX has it's own syntax

# alan@gmail.com
# Mr. Alan Turing
# 022 2312 2342
# asnldjasndkljsdncZASc

# https://pythex.org/


# alan@gmail.com

# alan.turing@gmail.com
# alan123.turi3@gmail.com
# alan123.turi3@something.co

# alan-123@gmx-mail.com
# alan123gmail.com@


# Steps/formula to check if an email is valid

# 1. Starts with 1 or more letter, number, _, -, .
# 2. A single @ sign
# 3. One or more letters, numbers, or -  [no special characters in the domain]
# 4. A single dot .
# 5. ends with 1 or more letters, number, - or .

# rstforum.net
# students.rstforum.net


# alan.turing@gmail.com
# (^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)


# ^[a-zA-Z0-9_.+-]+
# @
# [a-zA-Z0-9-]+
# \.
# [a-zA-Z0-9-.]+
# $


# Base REGEX Patterns

# \d    digit 0-9
# \w    letters, digits, underscore
# \s    whitespace
# \D    not a digit
# \W    not a word character
# \S    not a whitespace character
# .     any character except line break


# REGEX Quantifiers

# +     One or more
# {3}   Exactly x time. ex. {3} - 3 times
# {3,9} Three to nine times
# {4,}  Four or more times
# *     Zero or more times
# ?     Once or none


# py\w+
# h\w{4}

# \d{3}\s\d{4}\s\d{4}
# \d{3}\s?\d{4}\s?-?\d{4}       <- optional values ?

# \w{8,10}
# \w{8,}
# ab*c


# Character classes and sets
# One of the characters in the brackets will be checked

# [aeiou]
# [aeiou]{2}        <- to match double vowels here

# We can check for RANGES of character
# [a-z]         any lowercase letter
# [A-Z]         any uppercase letter
# [a-f]         only lowercase letter between a and f
# [a-z]{2,}     Two or more lowercase characters
# [a-zA-Z]{2,}
# [0-9]{2,}
# [a-z0-9]
# [a-z0-9]{4}

# [^d]          Not d.... (NOT - the carret (^) inside the [])
# [^0-9aeiou]   anything that is not 0-9 or a,e,i,o,u
# [^0-9aeiou\s@]


# ########### Anchors and Boundaries

# ^     Start of string or line
# $     End of string or line
# \b    Word boundary

# \band\b

# LOGICAL OR - |

# Mr|Mrs|Ms


# (\(\d{3}\)|\d{3})\s\d{4}\s\d{4}


# Group area code condition and the rest of the phone
# (\(\d{3}\)|\d{3})(\s\d{4}\s\d{4})


# Prefix First name and last name
# (Mr\.|Mrs\.)\s([A-Za-z]+)\s([A-Za-z]+)


# Python RE module - https://docs.python.org/3/library/re.html
# r stands for raw string

import re

# res = []

# pattern = re.compile(r"\d{3}\s\d{4}-\d{4}")
# # message = "Call us today at 022 2342-2342"
message = "Call us today at 022 2342-2342 or 022 2345-2345"

# res = pattern.search(message)
# res = pattern.findall(message)
# # print(res.group())
# print(res)


# print(pattern.search(message).group())

print(re.search(r"\d{3}\s\d{4}-\d{4}", message).group())
print(re.search(r"\d{3}", message).group())
print(re.search(r".", message).group())
print(re.search(r"\w", message).group())
import re


def extract_phone(input):
    phone_regex = re.compile(r"\b\d{3} \d{4}-\d{4}\b")
    match = phone_regex.search(input)

    if match:
        return match.group()
    return None


# print(extract_phone("This is my phone no 022 2133-3243"))


def extract_all_phone(input):
    phone_regex = re.compile(r"\b\d{3} \d{4}-\d{4}\b")
    return phone_regex.findall(input)


# print(extract_all_phone("My number is 022 2444-2345 or call me on 022 2456-7567"))


def is_valid_phone(input):
    phone_regex = re.compile(r"^\d{3} \d{4}-\d{4}$")
    match = phone_regex.search(input)

    if match:
        return True
    return False


print(is_valid_phone("022 3244-3242"))


# A complete

# Data base
# Ask the user to enter Employee Name (Mr. Alan Turing) -> Mr. Alan Turing
# Validate that details => print that it's invalid entry -> do you want to add again
# Ask the user to enter birthdate (23-12-2020) -> 23-12-2020
# Ask multiple fields
# Ask user (save, discard)
# if save -> append this data to a csv file
# if discard -> then exit -> print "Would you want to start again?"


# suggestion ->


# Class --
# @validate("\b\d{3} \d{4}-\d{4}\b")
# get_full_name(input)


# class Database:
#     @validate(r"^\d{3} \d{4}-\d{4}$")
#     def get_full_name():
#         user_input = input("Please add your full name (format = Mr. John Doe): ")

# [name, description, job_title\n]
# [alan, works for the government, analyst\n]
# [jack, works for the NSA, engineer\n]


# Databases

# SQL, NoSQL

# Tables and Columns
# How many columns will it have, what kind of data will be there in each column -> Schema
# MySQL, MarianDB, OracleSQL, Microsoft SQL, PostgreSQL, SQLite

# SQLite

# Scalable ->

# NOSQL -> MongoDB, CockroachDB, Firestore


# To view all your tables -> .tables
# SQL Syntax

# To create a new database, go to the folder in your terminal
# sqlite3 NAME_OF_DB.db

# CREATE TABLE products (name TEXT, description TEXT, price REAL, quantity INTEGER);

# Insert rows of data in our table

# INSERT INTO products (name, description, price, quantity) VALUES ("Apple iPhone", "A smartphone", 1000, 99);
# INSERT INTO products (name, description, price, quantity) VALUES ("Samsung Galaxy", "Another smartphone", 1400, 200);
# INSERT INTO products (name, description, price, quantity) VALUES ("Samsung M40", "A cheap samsung smartphone", 499, 140);
# INSERT INTO products (name, description, price, quantity) VALUES ("One Plus 11", "A chinese smartphone", 899, 400);
# INSERT INTO products (name, description, price, quantity) VALUES ("Microsoft Surface Duo", "A phone, tablet hybrid", 1600, 50);


# Read / Selecting


# SELECT * FROM products;
# SELECT * FROM products WHERE name IS "One Plus 11";
# SELECT * FROM products WHERE name IS NOT "Apple iPhone";
# SELECT * FROM products WHERE price < 1000;
# SELECT * FROM products WHERE name LIKE "%Sa%"

# Delete
# DELETE FROM products;
# DELETE FROM products WHERE quantity < 300;


# import sqlite3

# conn = sqlite3.connect("prodcuts_db.db")
# c = conn.cursor()

# # To make a new TABLE
# # c.execute("CREATE TABLE customers (name TEXT, age INTEGER, address REAL)") # should only happen once

# form_name = "Alan"

# # data = ("Jake Gyllenhal", 42, "Downtown LA, California CA, US")

# data = [
#     ("Lucifer Morningstar", 42, "Downtown LA, California CA, US"),
#     ("Maggie Gyllenhal", 23, "Rochester, New York"),
#     ("Wes Stephens", 65, "Wisconson Nathan, MC"),
#     ("Alan Turing", 23, "Lower Manhattan, NY, NY")
# ]
# # query = f"INSERT INTO customers (name) VALUES (\"{data[0]}\")"

# # query = "INSERT INTO customers (name, age, address) VALUES (?, ?, ?)"
# # c.execute(query, data)

# # c.executemany(query, data)

# # average_age = 0
# # for user in data:
# #     c.execute(query, user)
# #     print(f"Saving {user[0]} to database")
# #     average_age += user[1]

# # print(f"The users have an average age of {average_age/len(data)}")

# # SELECTING
# query = "SELECT * from customers WHERE name='Lucifer Morningstar'"
# c.execute(query)
# # print(c)

# # for row in c:
# #     print(row)

# # print(c.fetchall())
# print(c.fetchone())


# conn.commit()
# conn.close()


# import sqlite3

# conn = sqlite3.connect("users.db")
# c = conn.cursor()

# # Create table
# # init_query = "CREATE TABLE users (username TEXT, password TEXT)"
# # c.execute(init_query)

# # Bulk insert data
# # data = [
# #     ("alan", "dasdkj23n4"),
# #     ("mary", "asdasdas"),
# #     ("jack", "asdasmkn2"),
# #     ("jim", "password"),
# #     ("john", "1234")
# # ]
# # c.executemany("INSERT INTO users VALUES (?,?)", data)

# # Retrive data
# u = input("Please enter your username: ")
# p = input("Please enter your password: ")  # ' OR 1=1--


# # # SQL Injection
# # query = f"SELECT * FROM users WHERE username='{u}' AND password='{p}'"
# # c.execute(query)

# # Correct way (safer)
# query = "SELECT * FROM users WHERE username=? AND password=?"
# c.execute(query, (u, p))


# result = c.fetchone()
# # print(result)

# if result:
#     print("Welcome back")
# else:
#     print("Login failed")


# conn.commit()
# conn.close()


# # SELECT * FROM users WHERE username='john' AND password='' OR 1=1 --'

# # SELECT * FROM users WHERE username='john' AND password='' OR 1=1


# DECORATOR REVISION
# Higher Order Function
# Take a function as an argument and it will return a function back


# # decorator
# def extra_greet(fn):
#     def wrapper():
#         print("Whats up!")
#         fn()
#     return wrapper


# @extra_greet
# def greet():
#     print("Hello World!")


# # decorator
# def extra_greet(fn):
#     def wrapper():
#         print("Whats up!")
#         result = fn()
#         return f"This is something cool - {result}"
#     return wrapper


# @extra_greet
# def greet(name):
#     return f"Hello World! {name}"


# print(greet("Rahul"))

excluded_food = input("Please enter the excluded food: ")


def ensure_first_arg_is(food):
    def inner(fn):
        def wrapper(*args, **kwargs):
            # print(args)
            if args[0] != food:
                # print("ERROR BLOCK")
                return "ERROR"
            else:
                # print("TRUTH BLOCK")
                return fn(*args, **kwargs)
        return wrapper
    return inner


@ensure_first_arg_is(excluded_food)
def fav_food(*args):
    print([i for i in args])
    # pass


print(fav_food("water", "carrots", "pizza", "spinach"))

# Install Scrapy
# pip install scrapy


import scrapy


class BookSpider(scrapy.Spider):
    name = "bookspider"
    start_urls = ["http://books.toscrape.com"]

    def parse(self, response):
        for article in response.css("article.product_pod"):
            yield {
                "price": article.css(".price_color::text").extract_first(),
                "title": article.css("h3 > a::attr(title)").extract_first()
            }
        next = response.css(".next > a::attr('href)").extract_first()

        if next:
            yield response.follow(next, self.parse)


# scrapy runspider -o sampletextscrape.csv .\SP21_scp_cwl.py


# # REST
# API


# Pokemon Class game -> end points

# pokemoncrater.com
# request.get(api.weather.com/weather/dasdmlasdfmersdefsfsd8ccxc)


# request.get()

# get       request
# post      request
# put       request
# delete    request


# Blog Article @3
1. Write a function called repeat, which accepts a string and a number and returns a new string with the string passed to the function repeated the number amount of times. Do not use the built in repeat method!

# repeat('*', 3) # '***'


2. Write a function called list_check, which accepts a list and returns True if each value in the list is a list. Otherwise the function should return False.

# list_check([[],[1],[2,3], (1,2)]) # False


3. Write a function called vowel_count that accepts a string and returns a dictionary with the keys as the vowels and values as the count of times that vowel appears in the string.

# vowel_count('awesome') # {'a': 1, 'e': 2, 'o': 1}

4. Write a Indian phone number validator


5. Write a function called find_and_replace, which takes in a file name, a word to search for, and a replacement word. Replaces all instances of the word in the file with the replacement word.


6. Write a function called copy_and_reverse, which takes in a file name and a new file name and copies the reversed contents of the first file to the second file.


7. Simple RPG Game
Define a base class "Character" that has the following properties:
name - String
hp - an Integer value representing health (aka hitpoints)
level - an integer value representing experience level

Define a subclass "NPC" (which stands for Non-Player Character) that inherits from Character, and has an additional instance method called speak which prints the speech that character would say when a player interacts with them.

---

8.1. Create a class to represent a Pokémon.

a. It should include at least the following fields for:

i.	Name
ii.	Type - it represents different types of Pokémon: NORMAL, FIRE, GRASS, and WATER, and should always be initialized NORMAL.
iii.Attack
iv.	Health

b. As always, use proper encapsulation and include getters and setters for each value as appropriate.

c.At least one constructor.

d Crea a method that is called when the Pokémon is attacked by some other Pokémon. The method should define a Pokemon parameter and deduct the other Pokémon’s attack from this Pokémon’s health. The Pokémon’s health should not be reduced below 0. The method should return the amount of damage done.

f.If a Pokémon’s health is reduced to 0, the Pokémon loses consciousness. Write a method that returns true if the Pokémon is conscious, and false otherwise.


8.2  Use the table below to create three subclasses of the Pokémon class (one each for FIRE, GRASS, and WATER). This will require you to overridesome of the methods in your base Pokémon class. How will the child class change the hit points in the parent class when attacked?

Enemy Pokémon Type (subtract this much damage)

Pokémon Type	    Normal		  Fire		  Grass		  Water
Normal		        attack		attack		  attack		attack
Fire		          attack		attack		  ½ attack	2X attack
Grass		           attack		2X attack	  attack½ 	attack
Water		          attack½ 	attack2X 	  attack		attack


8.3 Implement a class called Arena that declares a single method with two Pokémon parameters. Implement the method such that, given two different Pokémon, it does the following:

    a.Prints the name, type, attack, and health of each pokemon.
    b.In a loop, pits the Pokémon against each other.
      i.  The first Pokémon attacks the second. Print a detailed message including damage done, remaining health, etc.
      ii. The second Pokémon attacks the first. Print a detailed message including damage done, remaining health, etc.
      iii.Check to see if either (or both) Pokémon is knocked out. If so, print a message and end the loop.


---

9 Write a function called letter_counter which accepts a string and returns a function. When the inner function is invoked it should accept a parameter which is a letter, and the inner function should return the number of times that letter appears. This inner function should be case insensitive.

# counter = letter_counter('Hello')
# counter('l') # 2


10. Write a function called next_prime, which returns a generator that will `yield` unlimited number of primes.

primes = next_prime()

next(primes) #

[next(primes) for i in range(25)]
# [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]