Full Stack Software Engineering for Beginners

July 21st, 2023


Introduction to Backend development (cont’d)

Introduction to Python Flask

Definition and Purpose

Flask is a lightweight and flexible web application framework written in Python. It is designed to be simple and easy to use, allowing developers to build web applications quickly and efficiently.

Advantages of Python Flask

  1. Minimalistic and Unopinionated: Flask follows a minimalistic design philosophy, giving developers the freedom to structure applications as they prefer without imposing rigid conventions.
  2. Extensible and Modular: Flask allows developers to add third-party extensions and libraries to enhance functionality, making it highly modular and scalable.
  3. Built-in Development Server: Flask comes with a built-in development server, which allows developers to test and run applications locally during development.

Setting Up Python Flask

  1. Installation: Flask can be installed using pip, the Python package manager. Run pip install Flask in the terminal or command prompt to install Flask.
  2. Verification: After installation, verify that Flask is correctly installed by running a simple “Hello, Flask!” application.

Creating a Simple Flask Server

Setting Up a Project

  1. Create a new directory for the project.
  2. Navigate to the project directory in the terminal or command prompt.

Create a Simple Flask Application

  • Create a new Python file (e.g., app.py) in the project directory.
  • Import Flask and create an instance of the application.
  • Define a route to handle HTTP GET requests to the root path (‘/’) and send a response.
  • Start the server and run the application.

Example:

from flask import Flask
app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, Flask!'

if __name__ == '__main__':
    app.run(debug=True)

Run the Flask Server

  • In the terminal, run python app.py to start the Flask server.

Access the Server

  • Open a web browser and navigate to http://localhost:5000 (assuming the server is running on the default port 5000). You should see the message “Hello, Flask!” displayed.

Exploring Routing and URL Parameters in Flask

Routing in Flask

  • Flask uses the @app.route() decorator to define routes and associate them with route handlers (view functions).

Example:

@app.route('/greet/<name>')
def greet(name):
    return f'Hello, {name}!'

URL Parameters in Flask

  • Flask supports URL parameters denoted by angle brackets <param_name> in the route URL. These parameters are extracted and made available in the route handler as arguments.

Example:

@app.route('/user/<int:user_id>')
def get_user(user_id):
    # Retrieve user data using the user_id parameter
    return f'User ID: {user_id}'

Utilizing Flask Templates and Static Files

Flask Templates

  • Flask supports templates using Jinja2, a powerful and feature-rich template engine. Templates allow developers to create dynamic HTML pages by injecting data from the server into the HTML.

Example:

from flask import render_template

@app.route('/greet/<name>')
def greet(name):
    return render_template('greet.html', name=name)

Static Files in Flask

  • Flask can serve static files, such as CSS, JavaScript, and images, by placing them in a folder named “static” within the project directory.

Example:

<!-- greet.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Greeting Page</title>
    <link rel="stylesheet" href="">
</head>
<body>
    <h1>Hello, !</h1>
    <img src="" alt="User Avatar">
</body>
</html>

Tips for writing a code with python

  1. Modular Code Structure: Divide your code into modules or files based on their functionality. For example, you can have separate files for utility functions, database interactions, API routes, and business logic. This makes it easier to manage and understand different parts of the application.

  2. Create utils.py for Utility Functions:

  • Create a utils.py file to store utility functions that are used across different parts of your application.

  • Example utils.py:

# utils.py
def calculate_average(numbers):
    return sum(numbers) / len(numbers)

def generate_random_string(length):
    # Code to generate a random string of the specified length
    pass
  • Import and use the utility functions in other parts of the application:
# other_module.py
from utils import calculate_average

data = [10, 20, 30, 40, 50]
average = calculate_average(data)
  1. Create db.py` for Database Interactions:
  • Create a db.py` file to handle database-related operations like connecting to the database, querying, and updating data.

  • Example db.py:

# db.py
import sqlite3

def connect_to_database():
    # Code to establish a connection to the database
    pass

def execute_query(query, params=None):
    # Code to execute a database query with optional parameters
    pass

def get_user_by_id(user_id):
    # Code to retrieve a user from the database by their ID
    pass
  • Import and use the database functions in other parts of the application:
# other_module.py
from db import connect_to_database, get_user_by_id

# Connect to the database
db_conn = connect_to_database()

# Get a user by ID
user_id = 1
user = get_user_by_id(user_id)
  1. Avoid Global Variables: Minimize the use of global variables as they can lead to unexpected behavior and make the code harder to maintain. Instead, pass data explicitly through function arguments or use class-based approaches for more complex scenarios.
# Avoid this:
my_variable = 10

def my_function():
    global my_variable
    my_variable += 1

# Prefer this:
def my_function(my_variable):
    return my_variable + 1

new_value = my_function(10)
  1. Use Classes for Complex Logic: For more complex business logic or operations that involve multiple functions and states, consider organizing them into classes. Classes can provide a cleaner and more structured way to manage complex backend logic.
class Calculator:
    def add(self, x, y):
        return x + y

    def subtract(self, x, y):
        return x - y

calc = Calculator()
result = calc.add(5, 3)  # Output: 8
  1. Error Handling: Implement robust error handling to gracefully handle exceptions and prevent unexpected crashes. Use try, except, and finally blocks to catch and handle errors appropriately.
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print("Error: Division by zero.")
finally:
    print("Finally block executed.")
  1. Logging: Use Python’s built-in logging module to log relevant information, warnings, and errors. Proper logging helps in debugging and understanding the application’s behavior during runtime.
import logging

logging.basicConfig(level=logging.INFO)

logging.debug('Debug message')
logging.info('Info message')
logging.warning('Warning message')
logging.error('Error message')
logging.critical('Critical message')
  1. Unit Testing: Write unit tests for your backend code using Python’s built-in unittest or third-party testing frameworks like pytest. Testing ensures that your code behaves as expected and allows for easier refactoring without breaking existing functionality.
# my_math.py
def add(x, y):
    return x + y

# test_my_math.py
import unittest
from my_math import add

class TestMyMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(3, 5), 8)
        self.assertEqual(add(-1, 1), 0)

if __name__ == '__main__':
    unittest.main()
  1. Virtual Environments: Use virtual environments (e.g., venv or virtualenv) to isolate project dependencies and avoid conflicts with other projects. Virtual environments allow you to manage package versions and dependencies more effectively.
# Create and activate a virtual environment
python -m venv myenv
source myenv/bin/activate   # On Windows: myenv\Scripts\activate

# Install dependencies
pip install flask
  1. Documentation: Add meaningful comments and docstrings to explain the purpose and usage of functions, classes, and modules. Proper documentation makes it easier for other developers (including your future self) to understand the codebase.
def calculate_average(numbers):
    """
    Calculate the average of a list of numbers.

    Parameters:
        numbers (list): A list of numerical values.

    Returns:
        float: The average value.
    """
    return sum(numbers) / len(numbers)

Building a simple server-side application

Introduction to Server-Side Application Development

Definition and Purpose

Server-side applications are programs that run on the server and handle client requests, process data, and generate responses. They provide the core functionality and business logic for web applications.

Setting Up the Project

Create a New Project Directory

  • Create a new directory for the project to keep all files organized.

Initialize a New Project

  • Depending on the chosen technology, initialize a new project with the appropriate tools and package manager (e.g., npm init, pipenv, bundle init).

Building the Server-Side Application with Python Flask (Example)

Install Flask

  • Use the package manager (pip or pipenv) to install Flask.

Create a Simple Flask Application

  • Create a new Python file (e.g., app.py) and import Flask.
  • Define a route to handle HTTP GET requests to the root path (‘/’) and send a response.

Example app.py:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello, Flask!'

Run the Flask Server

  • In the terminal, run python app.py to start the Flask server.

Access the Server

  • Open a web browser and navigate to http://localhost:5000 (assuming the server is running on the default port 5000). You should see the message “Hello, Flask!” displayed.

Adding the “Adding Friend” Feature (Example)

Implement the “Adding Friend” Route

  • Define a route to handle HTTP POST requests to add a friend and return a success message.

Example app.py:

from flask import Flask, request, jsonify

app = Flask(__name__)

friends = []

@app.route('/add_friend', methods=['POST'])
def add_friend():
    data = request.json
    friend_name = data.get('name')

    if friend_name:
        friends.append(friend_name)
        return jsonify({'message': f'{friend_name} added as a friend.'})
    else:
        return jsonify({'error': 'Name not provided.'}), 400

Test the “Adding Friend” Feature

  • Use a tool like curl or Postman to send a POST request to http://localhost:5000/add_friend with a JSON payload containing the friend’s name.
  • The server will respond with a success message if the friend is added successfully.

Authorizing Users with Bearer Token (Example)

Install Required Libraries

  • Install the required libraries, such as Flask-HTTPAuth, to implement token-based authentication.

Create an Authentication Endpoint

  • Define a route to handle user login and issue a Bearer token upon successful authentication.

Example app.py:

from flask import Flask, jsonify, request
from flask_httpauth import HTTPBasicAuth, HTTPTokenAuth

app = Flask(__name__)
auth = HTTPBasicAuth()
token_auth = HTTPTokenAuth(scheme='Bearer')

# In a real-world scenario, usernames and passwords should be stored securely.
users = {
    "john": "password123"
}

@auth.verify_password
def verify_password(username, password):
    if username in users and users[username] == password:
        return username

@app.route('/login')
@auth.login_required
def login():
    token = auth.current_user()
    return jsonify({'token': token})

@app.route('/protected')
@token_auth.login_required
def protected_resource():
    return jsonify({'message': 'This is a protected resource.'})

Test Token-Based Authentication

  • Send a GET request to http://localhost:5000/login with Basic Auth credentials (username and password).
  • The server will respond with a Bearer token.
  • Use the received token to access the protected resource by sending a GET request to http://localhost:5000/protected with the token in the Authorization header (e.g., Authorization: Bearer ).

Authentication techniques

Authentication is a crucial aspect of web applications, ensuring that users are who they claim to be. There are various types of authentication techniques, each with its own strengths and use cases. Here are some common authentication techniques with examples:

  1. Basic Authentication:
  • Basic authentication involves sending the username and password in the HTTP request headers, encoded in Base64.
  • It is simple but not secure as the credentials are transmitted as plaintext.
  • Example using Python’s requests library:
import requests
from requests.auth import HTTPBasicAuth

url = 'https://api.example.com/data'
response = requests.get(url, auth=HTTPBasicAuth('username', 'password'))
  1. Token-Based Authentication:
  • Token-based authentication involves issuing a token to a user upon successful login. The token is then sent with each subsequent request to authenticate the user.
  • The token is typically a long, random string, and it can be stored in local storage, session storage, or as an HTTP-only cookie in the browser.
  • Example using JSON Web Tokens (JWT) with Python’s Flask-JWT-Extended library:
from flask import Flask, jsonify, request
from flask_jwt_extended import JWTManager, jwt_required, create_access_token

app = Flask(__name__)
app.config['JWT_SECRET_KEY'] = 'your-secret-key'
jwt = JWTManager(app)

@app.route('/login', methods=['POST'])
def login():
    username = request.json.get('username')
    password = request.json.get('password')

    # Perform authentication and verify credentials
    if verify_credentials(username, password):
        access_token = create_access_token(identity=username)
        return jsonify(access_token=access_token), 200
    else:
        return jsonify(message='Invalid credentials'), 401

@app.route('/protected', methods=['GET'])
@jwt_required()
def protected_resource():
    return jsonify(message='This is a protected resource.'), 200
  1. OAuth 2.0:
  • OAuth 2.0 is an authorization framework used to grant limited access to a user’s resources on one website to another website without sharing the user’s credentials.
  • It is commonly used for third-party applications to access user data without requiring the user’s username and password.
  • Example using OAuth 2.0 with Google Sign-In:
<!-- Include Google Sign-In library -->
<script src="https://apis.google.com/js/platform.js" async defer></script>

<!-- Sign-in button -->
<div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div>

<!-- JavaScript callback function -->
<script>
function onSignIn(googleUser) {
    // Get the user's Google ID token
    var id_token = googleUser.getAuthResponse().id_token;
    // Send the token to the server for validation and authentication
    // (Server-side validation is necessary to ensure the token is valid)
    $.post('/google-login', { token: id_token }, function(response) {
        console.log(response);
    });
}
</script>
  1. Multi-Factor Authentication (MFA):
  • MFA involves using multiple authentication methods to verify a user’s identity. It provides an extra layer of security.
  • Common MFA methods include something the user knows (password), something the user has (smartphone for SMS or authentication app), and something the user is (fingerprint or face recognition).
  • Example using SMS-based MFA:
import boto3
import random

# Function to generate and send a verification code via SMS using AWS SNS
def send_verification_code(phone_number):
    sns = boto3.client('sns', region_name='us-east-1')  # Replace 'us-east-1' with your desired AWS region
    verification_code = str(random.randint(100000, 999999))
    
    message = f'Your verification code is: {verification_code}'
    sns.publish(PhoneNumber=phone_number, Message=message)

    return verification_code

# Function to verify the code provided by the user
def verify_code(phone_number, code):
    # Assuming you have stored the verification code somewhere for validation
    # You can use a database, cache, or even a global variable in a real application
    stored_code = '123456'  # Replace this with the actual stored code
    return code == stored_code

# Example usage
phone_number = '+1234567890'  # Replace this with the user's phone number

# Step 1: Send the verification code via SMS
verification_code = send_verification_code(phone_number)
print(f'Verification code sent to {phone_number}: {verification_code}')

# Step 2: User provides the verification code
user_provided_code = input('Enter the verification code received: ')

# Step 3: Verify the code provided by the user
if verify_code(phone_number, user_provided_code):
    print('Verification successful. User is authenticated.')
else:
    print('Verification failed. Invalid code.')


Full Stack Software Engineering for Beginners

July 25th, 2023


Introduction to Databases and SQL

Definition and Purpose of Databases

  • Databases are organized collections of data that are structured, stored, and managed for efficient retrieval and manipulation.
  • They serve as a reliable and centralized storage solution for applications, enabling data persistence and scalability.

Types of Databases

Relational Databases

  • Relational databases store data in tables with predefined schemas.
  • They use SQL (Structured Query Language) to manage and manipulate data.
  • Example: MySQL, PostgreSQL

NoSQL Databases

  • NoSQL databases store data in flexible, schema-less formats like JSON or BSON.
  • They are designed for handling unstructured or semi-structured data.
  • Example: MongoDB, Cassandra

SQL Basics

Creating a Database

CREATE DATABASE my_database;

Creating a Table

CREATE TABLE employees (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    age INT,
    department VARCHAR(50)
);

Inserting Data into a Table

INSERT INTO employees (id, name, age, department)
VALUES (1, 'John Doe', 30, 'HR');

Querying Data

SELECT * FROM employees;

Updating Data

UPDATE employees
SET department = 'Marketing'
WHERE id = 1;

Deleting Data

DELETE FROM employees
WHERE id = 1;

Database Management Systems (DBMS)

  • DBMS software is used to manage databases, ensuring data integrity, security, and efficient data retrieval.
  • It acts as an interface between the database and the application.
  • Examples: MySQL, PostgreSQL, Oracle

Normalization

  • Normalization is the process of organizing data in a database to eliminate redundancy and improve data integrity.
  • It involves breaking down large tables into smaller, related tables.
  • Example: Convert a single “users” table to separate “users” and “addresses” tables.

Indexing

  • Indexing is a technique used to improve database query performance by creating data indexes.
  • Indexes provide quick access to specific columns, reducing the need for full-table scans.
  • Example: Creating an index on the “email” column in a “users” table.

Transactions

  • Transactions are sequences of database operations treated as a single unit.
  • They ensure data consistency and integrity in multi-step operations.
  • Example: Money transfer between bank accounts.

Setting Up a Database and Performing CRUD Operations

Introduction

In this class material, we will learn how to set up a database and perform CRUD (Create, Read, Update, Delete) operations using SQL. We will use a relational database (e.g., MySQL) as an example, but the concepts apply to other database management systems as well.

Setting Up the Database

Step 1: Install and Set Up a Database Server

  • Install the desired database server software (e.g., MySQL, PostgreSQL).
  • Set up the necessary configurations, such as setting the root password.

Step 2: Create a Database

  • Use the database management tool (e.g., MySQL shell) to create a new database.
CREATE DATABASE my_database;

Step 3: Create Tables

  • Define the schema for each table and create them within the database.
USE my_database;

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    age INT
);

CRUD Operations

Create (INSERT) Operation

  • Insert new records into the table.
INSERT INTO users (name, email, age) VALUES ('John Doe', 'john@example.com', 30);

Read (SELECT) Operation

  • Retrieve data from the table.
SELECT * FROM users;
  • Retrieve specific records based on conditions.
    SELECT * FROM users WHERE age > 25;
    

Update Operation

  • Modify existing records in the table.
UPDATE users SET age = 31 WHERE name = 'John Doe';

Delete Operation

  • Remove records from the table.
DELETE FROM users WHERE age < 18;