Review Topics

All of the topics below are things that have been heavily covered and used throughout the class. We will mostly be focusing on more complicated uses and mechanics of these topics.

Lists

  • What are Lists?
    • Lists are an ordered sequence of elements, where each element is a variable
    • Unlike dictionaries, lists' keys are all integers that describe the order of the list

Some examples of lists:

  • Playlist of songs
  • names of students in a class
  • contacts on your phone

  • Each element of a string is referenced by an index (which is a number) and they generally start 0 but for the AP Exam it starts at 1.

    • AP Exam: 1,2,3,4 etc.
    • Python: 0,1,2,3 etc.

How do lists Manage Complexity of a program?

  • We may not need as many variables. For example:
    • One Variable that holds all students would be better than having a variable for EACH student
    • There can also be a list of test scores so if they need to be curved then the same calculation can be applied to the list (that has all the students) instead of doing the calculations one at a time

Answer the following questions about the code block below:

  • Why do you think lists are helpful? What word does College Board like to use to describe the function of lists?

Lists are helpful because they prevent you from having to make a bunch of variables. You can also perform operations on all the elements in a list all at once.

# variable of type string
name = "Sri Kotturi"
print("name", name, type(name))

# variable of type integer
age = 16
print("age", age, type(age))

# variable of type float
score = 90.0
print("score", score, type(score))

print()

# variable of type list (many values in one variable)
langs = ["Python", "JavaScript", "Java", "Bash", "html"]
print("langs", langs, type(langs))
print("- langs[2]", langs[2], type(langs[2]))

print()

# variable of type dictionary (a group of keys and values)
person = {
    "name": name,
    "age": age,
    "score": score,
    "langs": langs
}
print("person", person, type(person))
print('- person["name"]', person["name"], type(person["name"]))
name Sri Kotturi <class 'str'>
age 16 <class 'int'>
score 90.0 <class 'float'>

langs ['Python', 'JavaScript', 'Java', 'Bash', 'html'] <class 'list'>
- langs[2] Java <class 'str'>

person {'name': 'Sri Kotturi', 'age': 16, 'score': 90.0, 'langs': ['Python', 'JavaScript', 'Java', 'Bash', 'html']} <class 'dict'>
- person["name"] Sri Kotturi <class 'str'>

Mathematical Expressions

What is the output of the cell below? What Mathematical Expressions do you see being used? (List them below.)

  • The output of the cell below is 10
  • I see the mathematical operations of addition and division being used.
grade1 = 10
grade2 =  grade1 

average_grade = (grade1 + grade2) // 2 #what are these two slashes?

print(average_grade)
10

What is the value of num1, num2, and num3? Explain how each number ended up what it was.

  • num1 = 4096
    • num1 was num2 (4) to the power of num3 (6), or 4^6. The result of 4^6 is 4096.
  • num3 = 1
    • % is the modulus operator in python
    • it returns the remainder of dividing the left hand operand by the right hand operand
    • num3 is set equal to the remainder of num1 / 5
    • 4096 / 5 = 819 R 1, therefore, num3 = 1
  • num2 = 455
    • num2 is the integer value num1 and num3 added together and then divided by 9
    • // basically divides two numbers, and it rounds the quotient to the nearest integer if there is a remainder
    • num1 + num3 = 4096 + 1 = 4097
    • 4097 / 9 = 455.2222
    • The nearest integer to 455.2222 is 455. Therefore, num2 = 455
num1 = 2
num2 = 4
num3 = 6
num1 = num2 ** num3
num3 = num1 % 5
num2 = (num1 + num3) // 9

print(num1)
print(num2)
print(num3)
4096
455
1

Selection

Selection refers to the process of making decisions in a program based on certain conditions. It is normally done with conditional statements.

Conditionals

What is a conditional?:

  • Statement that allows code to execute different instructions if a certain condition is true or false
  • Allows program to make decisions based on data and input

What are the main types of conditional statements?:

  • if
  • elif
  • else

If statements

  • The if statement is used to check if a certain condition is true. The condition can be any expression that evaulates to a boolean value, True or False. If the condition is True, then it executes a code block.
  • If (condition) then (consequence)
  • Example:
x = int(input("Enter a number"))
if x > 0: # if condition, check if this is true of false
    print("x is positive") # code that will execute if condition is met
x is positive

Else

  • The else statement executes a code block when the if condition is False.
  • If (condition) then (consequence A), else (consequence B)

Elif

  • The elif statement can check multiple conditions in a sequence, and execute a certain block of code if any of the conditions are true.
  • If (condition) then (consequence A), elif (condition) then (consequence B), else (consequence C)

  • Example adding onto the code from before to take negative numbers and 0 into account

x = int(input("Enter a number, x:"))
if x > 0: # if condition, check if this is true of false
    print("x is positive") # code that will execute if condition is met
elif x < 0: # if previous condition not true... elif condition, check if this is true of false
    print("x is negative")# code that will execute if condition is met
else: # everything else, in this case it is if x == 0 
    print("x is zero") # only executes if all previous conditions are not met
x is positive

Nested Conditionals

What is a nested conditional?:

  • Conditional statement inside another conditional statement
  • Allows to check for more complex conditions where one condition depends on another

Nested Conditional Statements

  • Example
x = int(input("Enter a number, x:"))
if x % 2 == 0:
    print("x is even divisible by 2")
    # only ever checks is x is divisble by 3 if x is even. nested conditional
    if x % 3 == 0:
        print("x is divisible by 3")
    else:
        print("x is not divisible by 3")
else:
    print("x is odd")

Indentation

When using conditionals and nested conditionals in Python, it is important to pay attention to the indentation in the code. The code inside the if, elif, and else blocks must be indented so they are nested wihtin the outer statements. This way, Python knows which code belongs to which block.

What is binary search and what is it used for?

  • Searching algorithm
  • Find and select a specific element in a sorted list of elements

How does binary search work?

  • Repeatedly divides the search interval in half to find the middle element and compares the middle value to the target value, if not the same then it continues on to either the lower or upper half
  • Eliminate half of the remaining search interval elements each time
  • Efficient way to search for element in large dataset

What is the time complexity and why?

  • O(log(N))
  • The maximum number of iterations is the amount of times the list can be divided in half until it reaches 1 number
  • Dividing by 2, so it is log2(N), logarigthm of n base 2

  • You may recognize the example below from the binary lesson last Friday

import random

def binary_search_game():
    low = 1
    high = 100
    target = random.randint(low, high)

    while True:
        guess = (low + high) // 2
        print(f"Is your number {guess}?")
        response = input("Enter 'higher', 'lower', or 'yes': ")

        # conditional statements to check target number and guess
        if response == 'yes':
            print(f"I guessed your number {guess}!")
            break
        elif response == 'higher':
            low = guess + 1
        elif response == 'lower':
            high = guess - 1
        else:
            print("Invalid input, please enter 'higher', 'lower', or 'yes'.")

binary_search_game()

Quick Hack

Write a program using conditionals and nested conditionals

  • Ideas: Quiz, game (rock paper scissors, guess number), etc
def greeter():

    while True:
        time = int(input("What time is it? Please input your response as a whole number between 0 and 23, in military time."))

        # conditional statements to check time
        if time < 12:
            print(f"It is {time} o'clock. Good morning!")
            break
        elif time < 18:
            print(f"It is {time} o'clock. Good afternoon!")
            break
        else:
            print(f"It is {time} o'clock. Good evening!")
            break

greeter()
It is 12 o'clock. Good afternoon!

Introduction to Algorithms

  • an algorithm is a set of instructions that describes how to solve a problem or perform a specific task using a computer program.
  • It is a precise sequence of computational steps that take an input and produce an output

How do Algorithms relate to data structures?

  • Algorithms often rely on specific data structures to solve problems efficiently.
  • Sorting algorithms require a data structure such as an array or a linked list to store and manipulate data.
  • Searching algorithms such as binary search require data structures like arrays or trees to organize and search through data.

Important Terms

What is an algorithm?

  • it is a finite set of instructions that accomplishes a specific task

Sequencing

  • means that there is an order in which to do things

Selection

  • Helps to choose two different outcomes based off of a decision that the programmer wants to make

Iteration

  • Repeat something until the condition is met. (also referred to as repetition)

Calling and Developing Procedures

  • A procedure is a sequence of instructions that performs a specific task.
  • To call a procedure, you need to know its name and any arguments it requires.
  • When a procedure is called, the program jumps to its instruction and starts executing it.
  • The arguments passed to a procedure can be used within the procedure to perform tasks or calculations.
  • After the procedure has completed its task, it returns control back to the calling program.
def add_numbers(a, b):
    sum = a + b
    print("The sum of", a, "and", b, "is", sum)

# Call the procedure with arguments 5 and 7
add_numbers(5, 7)
The sum of 5 and 7 is 12
  • The result of the procedure can be stored in a variable, printed to the screen, or used in any other way that is required by the program.
  • Procedures can be defined within the same program or in external files, and can be reused across multiple parts of the program.
  • To avoid errors and improve code readability, it's important to define and call procedures with proper syntax and conventions that are appropriate for the programming language you're using.
def calculate_average(numbers):
    total = sum(numbers)
    count = len(numbers)
    average = total / count
    return average

# Call the procedure with a list of numbers
numbers_list = [10, 20, 30, 40, 50]
result = calculate_average(numbers_list)

# Display the result
print("The average of", numbers_list, "is", result)
The average of [10, 20, 30, 40, 50] is 30.0

Algorithmic Efficiency

  • Algorithmic efficiency refers to the amount of time and resources needed to execute an algorithm.
  • The efficiency of an algorithm can be measured in terms of its time and space complexities.
    • Time complexity refers to the amount of time required by an algorithm to complete its task as a function of its input size.
    • Space complexity refers to the amount of memory required by an algorithm to complete its task as a function of its input size.
    • can be analyzed using Big O notation, which provides an upper bound on the worst-case time and space complexity of the algorithm.

What is the time complexity of the following code:

- O(N)
- O(N*log(N))
- O(N * Sqrt(N))
- O(N*N)
a = 0
for i in range(N):
  for j in reversed(range(i, N)):
    a = a + i + j

What will be the time complexity of the following code?

  • n
  • (n+1)
  • n(n-1)
  • n(n+1)

n(n-1)

value = 0
for i in range(n): #iterates "n" times, with "i" taking on values from 0 to n-1.
  for j in range(i): # iterates "i" times, with "j" taking on values from 0 to i-1.
    value=value+1
  • Efficiency can be improved by optimizing algorithms or by using more efficient data structures and algorithms.
    • Some common techniques for improving efficiency include reducing the size of input data, caching results, and parallelizing tasks.
    • Understanding algorithmic efficiency is important in software development, as it can impact the performance of applications and their ability to scale with larger data sets.

Iteration and Simulations

Simulations are models of real-world phonomena or systems that use mathematical algorithms and computer programs simulate the real behavior and aspects of the subject being modeled.

Simulations are most often used to model complex or time-consuming things that would be difficult to test in real life, such as modeling the spread of diseases in certain ecosystems or testing the functionality of a potential product before it is made.

In this lesson, we will be looking at lists, iteration, and random values through the lens of simulations.

PLEASE RUN THE CODE BELOW BEFORE INTERACTING WITH THE CODE SEGMENTS IN THIS SECTION!

class Card:
    def __init__(self, suit, val):
        self.suit = suit
        self.val = val
        if val == 11:
            self.kind = "Ace"
        elif val == 12:
            self.kind = "Jack"
        elif val == 13:
            self.kind = "Queen"
        elif val == 14:
            self.kind = "King"
        else:
            self.kind = str(self.val)

    #return a formatted string version of a card
    def show(self):
        return f"{self.kind} of {self.suit}"
    
    #adjust aces to prevent breaking
    def ace_adj(self):
        if self.kind == "Ace":
            self.val = 1

Review: Lists and Iteration

Lists and iteration work hand-in-hand to efficiently process and/or modify multiple values at once. In a card game, for example, lists and iteration are used together frequently to make the game work correctly.

For Loops

For loops are probably the most well-known type of iterative loop used in code. Most of us know about the for variable in list format.

One helpful tool not a lot of people know about is the enumerate() function. When used in conjunction with a for loop, you can always have access to the index and value of each selected list entry.

numlist = [3, 5, 68, 203]

for key, num in enumerate(numlist):
    print(f"This entry's index is {str(key)}, but its value is {str(num)}.")
    print(f"The difference between the value and the index is {num - key}.")
This entry's index is 0, but its value is 3.
The difference between the value and the index is 3.
This entry's index is 1, but its value is 5.
The difference between the value and the index is 4.
This entry's index is 2, but its value is 68.
The difference between the value and the index is 66.
This entry's index is 3, but its value is 203.
The difference between the value and the index is 200.

QUESTION: How is the key, num in enumerate(list) format similar to the format used when applying a for loop to a dictionary?

Answer:

  • you can use a for loop to look for specific values in a dictionary, such as for(i) in dictionary
  • it basically pulls the value that is at the index we specify
  • key, num in enumerate(list) basically also outputs the value that is at the index we specify

List Comprehension

You may also see for loops used within a list like below. We went over this in class fairly recently. In this case, it is used to show the cards in the hand of a player.

player_hand = [] # the player's hand is represented as a list
# because lists are mutable (can change), they can be added to, like drawing a card

# assume the deck below is a a deck of shuffled cards
deck = [Card("Hearts", 3), Card("Spades", 12), Card("Diamonds", 11)]
def draw_card(hand, deck):
    hand.append(deck.pop())

#try it out
draw_card(player_hand, deck)
print([card.show() for card in player_hand])
['Ace of Diamonds']

Recursive Loops

Recursive loops have you calling one function inside of another. If a function must make some change to a certain value multiple times, it is oftem most efficient to have a function call itself with slightly different arguments like the fibonacci sequence below.

def fibonacci(terms):
    if terms <= 1:
        return terms
    return fibonacci(terms-1) + fibonacci(terms-2)

fibonacci(5)
5

Nesting Loops

Nesting loops increases the time complexity of the program, but it can be used to do things like make a card deck (see below).

def build(deck):
        for suit in ["Spades", "Clubs", "Diamonds", "Hearts"]:
            for val in range(2, 15): #HINT: try replacing this function
                deck.append(Card(suit, val))

While Loops

While loops aren't used in the program, but they offer a different way to repeat a set of instructions in a program. The procedure below the while [condition] line will occur until the condition is made not true.

Student Interaction: How could this build function be altered to function with a while loop within it?

def build(deck):
    suits = ["Spades", "Clubs", "Diamonds", "Hearts"]
    values = list(range(2, 15))
    i = 0
    while i < len(suits):
        suit = suits[i]
        j=0
        while j < len(values):
                val = values[j]
                deck.append(Card(suit, val))
                j += 1
    i += 1

While loops also alter an alternative way to loop a set of instructions forever, until a precise thing occurs to break the loop. See the code below.

import random
i = 0

while True:
    i += 1
    ch = random.randint(1, 11)
    if ch == 10:
        print(f"It took {str(i)} random generations to get 10.")
        break
It took 13 random generations to get 10.

49 random generations is a lot more than it would normally take, but it's important for code to be able to model unlikely, yet possible scenarios. Speaking of random values...

Random Values

Because unpredictable randomness occurs in the real world, it's important to have a way to represent it. Simulations are able to use randomization, which could be in the form of random number generation or other methods like shuffle.

Card decks are a great example of how random values can be used to represent real-world scenarios. In the card simulation, the random module's shuffle function is used to quite literally shuffle the deck, seen below.

def shuffle(deck):
    random.shuffle(deck)

Often, random selection methods use functions like randint or randrange as ways to select certain indexes in lists, or might use the random numbers in some other way.

QUESTION: Without shuffling the card order of the deck, can you think of a way that the aforementioned random module functions could be used to get a random card from the deck? Do so in the code cell below.

import random

player_hand = [] # the player's hand is represented as a list
deck = [] # the deck is represented as an empty list
    
def build(deck):
        for suit in ["Spades", "Clubs", "Diamonds", "Hearts"]:
            for val in range(2, 15): #HINT: try replacing this function
                deck.append(Card(suit, val))

def draw_card(hand, deck):
    hand.append(deck.pop())

#try it out
build(deck)
draw_card(player_hand, deck)
print([card.show() for card in player_hand])
['14 of Hearts']

Simulation Homework

Now that you've learned about simulations and how they're used, it's time to apply that knowledge by creating a (basic) simulation of a real-world scenario. It can be something in nature, like the changes in the wildlife population of a certain area; it can be a game, like Uno (no blackjack though, that's taken); or it can be something completely random and unique.

The simulation must include...

  • Use of at least one random value
  • At least one list or similar data type (dictionary, set, etc.)
  • Efficient use of iteration (must support the purpose of the simualtion)
  • Selection (use of conditionals)

Do this in your student copy in the cell provided. This is worth 0.9 (or more with extra credit) out of the 3 possible points.

import random

# define the initial population and growth rate range
population = 30
min_growth_rate = 0.01
max_growth_rate = 0.8

# simulate population change over 30 years
population_history = [population]
for year in range(1, 30):
    # generate a random growth rate for each year
    growth_rate = random.uniform(min_growth_rate, max_growth_rate)
    
    # calculate the new population size
    growth = population * growth_rate
    population += growth
    
    # simulate random events that affect the population
    if random.random() < 0.1:
        # a disease outbreak occurs
        population *= 0.8
    elif random.random() < 0.05:
        # a food shortage occurs
        population *= 0.9
    
    # store the population size for this year
    population_history.append(int(population))
    
    # print the population size and growth rate for this year
    print("Year {}: population = {}, growth rate = {}".format(year, int(population), growth_rate))
    
# print the population history list
print(population_history)
Year 1: population = 53, growth rate = 0.7856327275271673
Year 2: population = 55, growth rate = 0.028945712566536802
Year 3: population = 61, growth rate = 0.39860810570366534
Year 4: population = 97, growth rate = 0.588568258972644
Year 5: population = 99, growth rate = 0.272908126391717
Year 6: population = 135, growth rate = 0.35616041327248465
Year 7: population = 211, growth rate = 0.5642718064576471
Year 8: population = 310, growth rate = 0.4667259713724124
Year 9: population = 286, growth rate = 0.15492984597319612
Year 10: population = 397, growth rate = 0.7304523796045101
Year 11: population = 597, growth rate = 0.5046443345454681
Year 12: population = 775, growth rate = 0.29746773271616406
Year 13: population = 902, growth rate = 0.1645134652558842
Year 14: population = 917, growth rate = 0.016617584448939036
Year 15: population = 941, growth rate = 0.026216152121490623
Year 16: population = 914, growth rate = 0.07877664590915705
Year 17: population = 1296, growth rate = 0.41834768352759477
Year 18: population = 1064, growth rate = 0.026090654974325292
Year 19: population = 1168, growth rate = 0.09789096180308442
Year 20: population = 1189, growth rate = 0.018055201856534888
Year 21: population = 1752, growth rate = 0.47298403177915244
Year 22: population = 2763, growth rate = 0.5770086586734327
Year 23: population = 4479, growth rate = 0.6208519137655218
Year 24: population = 7709, growth rate = 0.7209845027811035
Year 25: population = 8326, growth rate = 0.07997403951008215
Year 26: population = 10429, growth rate = 0.25262644983437926
Year 27: population = 13305, growth rate = 0.275691971934834
Year 28: population = 18828, growth rate = 0.4151413751873726
Year 29: population = 31131, growth rate = 0.6534159628686299
[30, 53, 55, 61, 97, 99, 135, 211, 310, 286, 397, 597, 775, 902, 917, 941, 914, 1296, 1064, 1168, 1189, 1752, 2763, 4479, 7709, 8326, 10429, 13305, 18828, 31131]

Databases

We have already gone over databases in this class, but here is a refresher. A database contains data that's stored in columns and rows. The information in this database can then be pulled from the database and can be used in a program.

Setting Up the Database

Run the code cell below to prepare SQLite to create the database. If your system is struggling with the flask functions, verify that you're in the correct Python environment. REMEMBER: You should only db.init_app(app) ONCE during the process!

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# Setup of key Flask object (app)
app = Flask(__name__)
# Setup SQLAlchemy object and properties for the database (db)
database = 'sqlite:///sqlite.db'  # path and filename of database
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = database
app.config['SECRET_KEY'] = 'SECRET_KEY'
db = SQLAlchemy()


# This belongs in place where it runs once per project
db.init_app(app)

The Model File

The model file plays a crucial role in the formation of the database.

  • The model helps to create new databases
  • It provides a standardized method for formating the database entries across different systems
  • Objects used within the database are created
import os, base64
import json
from sqlalchemy.exc import IntegrityError

# Define the User class to manage actions in the 'users' table
class User(db.Model):
    __tablename__ = 'players'  # table name is plural, class name is singular

    # Define the User schema with "vars" from object
    id = db.Column(db.Integer, primary_key=True)
    _username = db.Column(db.String(255), unique=False, nullable=False)
    _streak = db.Column(db.Integer, unique=True, nullable=False)

    # constructor of a User object, initializes the instance variables within object (self)
    def __init__(self, username, streak):
        self._username = username
        self._streak = streak

    # a username getter method, extracts username from object
    @property
    def username(self):
        return self._username
    
    # a setter function, allows username to be updated after initial object creation
    @username.setter
    def username(self, username):
        self._username = username
    
    # a getter method, extracts streak from object
    @property
    def streak(self):
        return self._streak
    
    # a setter function, allows streak to be updated after initial object creation
    @streak.setter
    def streak(self, streak):
        self._streak = streak
    
    # output content using str(object) in human readable form, uses getter
    # output content using json dumps, this is ready for API response
    def __str__(self):
        return json.dumps(self.read())

    # CRUD create/add a new record to the table
    # returns self or None on error
    def create(self):
        try:
            # creates a person object from User(db.Model) class, passes initializers
            db.session.add(self)  # add prepares to persist person object to Users table
            db.session.commit()  # SqlAlchemy "unit of work pattern" requires a manual commit
            return self
        except IntegrityError:
            db.session.remove()
            return None

    # CRUD read converts self to dictionary
    # returns dictionary
    def read(self):
        return {
            "id": self.id,
            "username": self.username,
            "streak": self.streak
        }

    # CRUD update: updates user name, password, phone
    # returns self
    def update(self, username, streak):
        """only updates values with length"""
        if len(username) > 0:
            self.username = username
        if streak > 0:
            self.streak = streak
        db.session.commit()
        return self

    # CRUD delete: remove self
    # None
    def delete(self):
        db.session.delete(self)
        db.session.commit()
        return None


"""Database Creation and Testing """

# Builds working data for testing
def initUsers():
    with app.app_context():
        """Create database and tables"""
        db.create_all()
        """Tester data for table"""
        u1 = User(username="Mr. Cards", streak=5)
        u2 = User(username="Kard Kowntre", streak=10)
        u3 = User(username="Un Bea Table", streak=15)

        users = [u1, u2, u3]

        """Builds sample user/note(s) data"""
        for user in users:
            try:
                user.create()
                print(f'Created user with username "{user.username}".')
            except IntegrityError:
                '''fails with bad or duplicate data'''
                db.session.remove()
                print(f"Records exist, duplicate email, or error: {user.username}")

The init Fuction

The init method has one purpose which is to initialize the object's attributes. This is what is known as the constructor. In our project, the init method initalizes the username and streak as variables.

def __init__(self, username, streak):
    self._username = username
    self._score = streak

Setters and Getters

Setters and Getters are important methods used when writing code for databases.

  • Setter: a method that allows us to set or change the value of an attribute in a class.
  • Getter: a method that allows us to access an attribute in a given class.

Setter Example

@streak.setter
def streak(self, streak):
self._streak = streak
  Cell In[74], line 4
    self._streak = streak
    ^
IndentationError: expected an indented block after function definition on line 3

Getter Example

@property
def streak(self):
return self._streak

The Api File

An API is an important part of having a functional database.

  • it acts as a messenger that allows programs to access data from the others
  • it connects all information
  • for a database, an api retrives the data requested in code for the user
import json
from flask import Blueprint, request, jsonify
from flask_restful import Api, Resource # used for REST API building

user_api = Blueprint('user_api', __name__,
                   url_prefix='/api/users')

api = Api(user_api)

class UserAPI:        
    class _CRUD(Resource):  # User API operation for Create, Read.  THe Update, Delete methods need to be implemeented
        def post(self): # Create method
            ''' Read data for json body '''
            body = request.get_json()
            
            ''' Avoid garbage in, error checking '''
            # validate name
            username = body.get('username')
            if username is None or len(username) < 1:
                return {'message': f'Username is missing, or is less than a character'}, 400
            # validate uid
            streak = body.get('streak')
            if streak is None or streak < 1:
                return {'message': f'Streak is missing, or is less than 1'}, 400

            ''' #1: Key code block, setup USER OBJECT '''
            uo = User(username=username, 
                      streak=streak)
            
            ''' #2: Key Code block to add user to database '''
            # create user in database
            user = uo.create()
            # success returns json of user
            if user:
                return jsonify(user.read())
            # failure returns error
            return {'message': f'Processed {username}, either a format error or a duplicate'}, 400

        def get(self): # Read Method
            users = User.query.all()    # read/extract all users from database
            json_ready = [user.read() for user in users]  # prepare output in json
            return jsonify(json_ready)  # jsonify creates Flask response object, more specific to APIs than json.dumps

        def put(self):
            body = request.get_json() # get the body of the request
            id = body.get('id')
            username = body.get('username')
            streak = body.get('streak') # get the UID (Know what to reference)
            user = User.query.get(id) # get the player (using the uid in this case)
            user.update(username=username, streak=streak)
            return f"{user.read()} Updated"

        def delete(self):
            body = request.get_json()
            id = body.get('id')
            player = User.query.get(id)
            player.delete()
            return f"{player.read()} Has been deleted"

    # building RESTapi endpoint
    api.add_resource(_CRUD, '/')

This is important particularly in a full flask respository context, but in this case, you'll just need to run the initUsers() function.

initUsers()
Created user with username "Mr. Cards".
Created user with username "Kard Kowntre".
Created user with username "Un Bea Table".

An Alternative Method of Making SQLite Databases

In a previous lesson, we went over using the cursor object in SQLite3. Rather than go over all of that here, this lesson goes over it thoroughly. (You may use this method for the homework below.)

Database Homework

For this assignment, we'd like you to make your own database file as instructed above. Remember, the API file isn't necessary in this case; you'll be focusing on making the model and the init function.

Your database must include these things:

  • A class with at least four attributes (if not the cursor method)
  • Setters and getters for this class (if not the cursor method)
  • Each of the CRUD functions
  • An init function with at least four entries
  • A screenshot showing proof that your SQLite file has been created correctly

Feel free to base your database on the model provided above! Ask our group if you have any questions or concerns.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy

# Setup of key Flask object (app)
app = Flask(__name__)
# Setup SQLAlchemy object and properties for the database (db)
database = 'sqlite:///sqlite.db'  # path and filename of database
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = database
app.config['SECRET_KEY'] = 'SECRET_KEY'
db = SQLAlchemy()


# This belongs in place where it runs once per project
db.init_app(app)d
import os, base64
import json
from sqlalchemy.exc import IntegrityError

# Define the User class to manage actions in the 'users' table
class Reader(db.Model):
    __tablename__ = 'readers'  # table name is plural, class name is singular

    # Define the User schema with "vars" from object
    id = db.Column(db.Integer, primary_key=True)
    _username = db.Column(db.String(255), unique=True, nullable=False)
    _name = db.Column(db.String(255), unique=False, nullable=False)
    _book = db.Column(db.String(255), unique=False, nullable=False)
    _year = db.Column(db.Integer, unique=False, nullable=False)
    _rating = db.Column(db.Integer, unique=False, nullable=False)

    # constructor of a User object, initializes the instance variables within object (self)
    def __init__(self, username, name, book, year, rating):
        self._username = username
        self._name = name
        self._book = book
        self._year = year
        self._rating = rating

    # a name getter method, extracts username from object
    @property
    def username(self):
        return self._username
    
    # a setter function, allows username to be updated after initial object creation
    @username.setter
    def username(self, username):
        self._username = username

    # a name getter method, extracts name from object
    @property
    def name(self):
        return self._name
    
    # a setter function, allows name to be updated after initial object creation
    @name.setter
    def name(self, name):
        self._name = name
    
    # a getter method, extracts book from object
    @property
    def book(self):
        return self._book
    
    # a setter function, allows book to be updated after initial object creation
    @book.setter
    def book(self, book):
        self._book = book
    
    # a getter method, extracts year from object
    @property
    def year(self):
        return self._year
    
    # a setter function, allows year to be updated after initial object creation
    @year.setter
    def year(self, year):
        self._year = year
    
    # a getter method, extracts rating from object
    @property
    def rating(self):
        return self._rating
    
    # a setter function, allows book to be updated after initial object creation
    @rating.setter
    def rating(self, rating):
        self._rating = rating
    
    # output content using str(object) in human readable form, uses getter
    # output content using json dumps, this is ready for API response
    def __str__(self):
        return json.dumps(self.read())

    # CRUD create/add a new record to the table
    # returns self or None on error
    def create(self):
        try:
            # creates a person object from User(db.Model) class, passes initializers
            db.session.add(self)  # add prepares to persist person object to Users table
            db.session.commit()  # SqlAlchemy "unit of work pattern" requires a manual commit
            return self
        except IntegrityError:
            db.session.remove()
            return None

    # CRUD read converts self to dictionary
    # returns dictionary
    def read(self):
        return {
            "id": self.id,
            "username": self.username,
            "name": self.name,
            "book": self.book,
            "year": self.year,
            "rating": self.rating
        }

    # CRUD update: updates user name, password, phone
    # returns self
    def update(self, username, name, book, year, rating):
        """only updates values with length"""
        if len(username) > 0:
            self.username = username
        if len(name) > 0:
            self.username = name
        if len(book) > 0:
            self.book = book
        if year > 0:
            self.year = year
        if 0 < rating < 5:
            self.rating = rating
        db.session.commit()
        return self

    # CRUD delete: remove self
    # None
    def delete(self):
        db.session.delete(self)
        db.session.commit()
        return None


"""Database Creation and Testing """

# Builds working data for testing
def initReaders():
    with app.app_context():
        """Create database and tables"""
        db.create_all()
        """Tester data for table"""
        r1 = Reader(username="rey444", name="Shreya", book ="The Hunger Games", year=2019, rating=5)
        r2 = Reader(username="jiyu", name="Jiya", book ="Divergent", year=2021, rating=3)
        r3 = Reader(username="vai", name="Vaishavi", book ="Animal Farm", year=2020, rating=4)

        readers = [r1, r2, r3]

        """Builds sample user/note(s) data"""
        for reader in readers:
            try:
                reader.create()
                print(f'Created user with username "{reader.username}".')
            except IntegrityError:
                '''fails with bad or duplicate data'''
                db.session.remove()
                print(f"Records exist, duplicate username, or error: {reader.username}")
import json
from flask import Blueprint, request, jsonify
from flask_restful import Api, Resource # used for REST API building

user_api = Blueprint('reader_api', __name__,
                   url_prefix='/api/readers')

api = Api(user_api)

class ReaderAPI:        
    class _CRUD(Resource):  # User API operation for Create, Read.  THe Update, Delete methods need to be implemeented
        def post(self): # Create method
            ''' Read data for json body '''
            body = request.get_json()
            
            ''' Avoid garbage in, error checking '''
            # validate username
            username = body.get('username')
            if username is None or len(username) < 1:
                return {'message': f'Username is missing, or is less than a character'}, 400
            # validate name
            name = body.get('name')
            if name is None or len(name) < 1:
                return {'message': f'Name is missing, or is less than a character'}, 400
            # validate book
            book = body.get('book')
            if book is None or len(book) < 1:
                return {'message': f'Book name is missing, or is less than a character'}, 400
            # validate year
            year = body.get('year')
            if year is None or year < 1900:
                return {'message': f'Year is missing, or is before 1900'}, 400
            # validate rating
            rating = body.get('rating')
            if rating is None or rating < 0:
                return {'message': f'Book rating is missing, or is below zero'}, 400
            elif rating is None or rating > 5:
                 return {'message': f'Book rating is missing, or is above five'}, 400

            ''' #1: Key code block, setup USER OBJECT '''
            uo = Reader(username=username, 
                      name=name,
                      book=book,
                      year=year,
                      rating=rating)
            
            ''' #2: Key Code block to add user to database '''
            # create user in database
            reader = uo.create()
            # success returns json of user
            if reader:
                return jsonify(reader.read())
            # failure returns error
            return {'message': f'Processed {username}, either a format error or a duplicate'}, 400

        def get(self): # Read Method
            readers = Reader.query.all()    # read/extract all users from database
            json_ready = [reader.read() for reader in readers]  # prepare output in json
            return jsonify(json_ready)  # jsonify creates Flask response object, more specific to APIs than json.dumps

        def put(self):
            body = request.get_json() # get the body of the request
            id = body.get('id')
            username = body.get('username')
            name = body.get('name')
            book = body.get('book') # get the book (Know what to reference)
            year = body.get('year')
            rating = body.get('rating')
            reader = Reader.query.get(id) # get the player (using the uid in this case)
            reader.update(username=username, name=name, book=book, year=year, rating=rating)
            return f"{reader.read()} Updated"

        def delete(self):
            body = request.get_json()
            id = body.get('id')
            reader = Reader.query.get(id)
            reader.delete()
            return f"{reader.read()} Has been deleted"

    # building RESTapi endpoint
    api.add_resource(_CRUD, '/')
initReaders()
Created user with username "rey444".
Created user with username "jiyu".
Created user with username "vai".

Create

import sqlite3

readers = 'instance/sqlite.db' # this is location of database

def schema():
    
    # Connect to the database file
    conn = sqlite3.connect(readers)

    # Create a cursor object to execute SQL queries
    cursor = conn.cursor()
    
    # Fetch results of Schema
    results = cursor.execute("PRAGMA table_info('users')").fetchall()

    # Print the results
    for row in results:
        print(row)

    # Close the database connection
    conn.close()
    
schema()
(0, 'id', 'INTEGER', 1, None, 1)
(1, '_name', 'VARCHAR(255)', 1, None, 0)
(2, '_uid', 'VARCHAR(255)', 1, None, 0)
(3, '_password', 'VARCHAR(255)', 1, None, 0)
(4, '_dob', 'DATE', 0, None, 0)
import sqlite3

def create():
    username = input("Enter your username:")
    name = input("Enter your name:")
    book = input("Enter the name of the book you would like to log:")
    year = input("Enter the year in which you read the book, in 'YYYY' format:")
    rating = input("Enter your rating of the book using numbers 0-5, with 0 being the worst and 5 being the best:")
    
    # Connect to the database file
    conn = sqlite3.connect(readers)

    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()

    try:
        # Execute an SQL command to insert data into a table
        cursor.execute("INSERT INTO readers (_username, _name, _book, _year, _rating) VALUES (?, ?, ?, ?, ?)", (username, name, book, year, rating))
        
        # Commit the changes to the database
        conn.commit()
        print(f"A new user record {username} has been created")
                
    except sqlite3.Error as error:
        print("Error while executing the INSERT:", error)


    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
create()
A new user record agustd has been created

Read

import sqlite3

def read():
    # Connect to the database file
    conn = sqlite3.connect(readers)

    # Create a cursor object to execute SQL queries
    cursor = conn.cursor()
    
    # Execute a SELECT statement to retrieve data from a table
    results = cursor.execute('SELECT * FROM readers').fetchall()

    # Print the results
    if len(results) == 0: # results has data in it, so it could be an object; results has attributes
        print("Table is empty")
    else:
        for row in results:
            print(row)

    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
read()
(1, 'rey444', 'Shreya', 'The Hunger Games', 2019, 5)
(2, 'jiyu', 'Jiya', 'Divergent', 2021, 3)
(3, 'vai', 'Vaishavi', 'Animal Farm', 2020, 4)
(4, 'thv', 'Tae', 'Vision', 2009, 5)

Update

import sqlite3

def update():
    username = input("Enter username to update")
    book = input("Enter updated book name")
    if len(book) < 2:
        message = "invalid"
        book = 'invalidbook'
    else:
        message = "successfully updated"

    # Connect to the database file
    conn = sqlite3.connect(database)

    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()

    try:
        # Execute an SQL command to update data in a table
        cursor.execute("UPDATE readers SET _book = ? WHERE _username = ?", (book, username))
        if cursor.rowcount == 0:
            # The uid was not found in the table
            print(f"No username {username} was not found in the table")
        else:
            print(f"The row with user id {username} the book has been {message}")
            conn.commit()
    except sqlite3.Error as error:
        print("Error while executing the UPDATE:", error)
        
    
    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
update()
The row with user id rey444 the book has been successfully updated

Delete

import sqlite3

def delete():
    username = input("Enter username to delete")

    # Connect to the database file
    conn = sqlite3.connect(readers)

    # Create a cursor object to execute SQL commands
    cursor = conn.cursor()
    
    try:
        cursor.execute("DELETE FROM readers WHERE _username = ?", (username,))
        if cursor.rowcount == 0:
            # The uid was not found in the table
            print(f"No uid {username} was not found in the table")
        else:
            # The uid was found in the table and the row was deleted
            print(f"The row with uid {username} was successfully deleted")
        conn.commit()
    except sqlite3.Error as error:
        print("Error while executing the DELETE:", error)
        
    # Close the cursor and connection objects
    cursor.close()
    conn.close()
    
delete()
The row with uid jiyu was successfully deleted

Grading

Your submission will be graded based on the following criteria:

  • Filling in the blank throughout the lesson and providing code in the given cells when applicable (0.9)
  • Simulation homework (0.9)
  • Database homework (0.9)

Here are some ideas for ways to increase your score above a 2.7:

  • Make a frontend version of your simulation that can be interacted with on your blog
  • Connect your simulation to the database you create
  • Create a menu that allows a user to make an entry in your database (CRUD functions within it)
  • You can establish a relationship between two classes/tables in your database (see the relationship between the User and Note classes in the Nighthawk Coders flask repository)