Step 1: Setting up the Database

Run Migration Script:

  • Run ./migrate.sh to create, migrate, and initialize the database.
  • The script checks for necessary dependencies: sqlite3, python3, and Flask.
  • It initializes the migration directory if it doesn’t exist and handles backup/restoration of the SQLite database.
# Check if the migration directory exists
if [ ! -d "migrations" ]; then
    echo "Initializing migration for the first time..."
    python3 -m flask db init
fi

# Check if sqlite.db does not exist and there is a sqlite-backup.db file
if [ ! -e "instance/volumes/sqlite.db" ] && [ -e "instance/volumes/sqlite-backup.db" ]; then
    echo "No sqlite.db found, using sqlite-backup.db to generate the file."
    
    # Copy backup file to primary (sqlite.db)
    cp "instance/volumes/sqlite-backup.db" "instance/volumes/sqlite.db"

    # Extract the new Alembic version from the backup database
    backup_version=$(sqlite3 instance/volumes/sqlite.db "SELECT version_num FROM alembic_version;")
    echo "Version ${backup_version} detected"

    python3 -m flask db stamp "${backup_version}"
fi

In this step, developers run the migration script to set up the database. The script initializes the migration directory if it’s the first time, and it checks for the existence of the SQLite database. If the primary database doesn’t exist but a backup is available, it restores from the backup and updates the Alembic version.

Step 2: Understanding the Code Structure

Environment Variables:

  • Setting FLASK_APP=main as an environment variable informs Flask about the main entry point of the application. PYTHONPATH=.:$PYTHONPATH specifies additional directories for the Python interpreter to locate modules and packages in the current project directory, enhancing module discovery during execution. Together, these environment variables streamline the execution of the Flask application.

Setting environment variables ensures that Flask can locate the main application (main) and include the project directory (.) in the Python path.

Dependency Checks:

# Check if sqlite3 is installed
if ! command -v sqlite3 &> /dev/null; then
    echo "Error: sqlite3 is not installed. Please install it before running this script."
    exit 1
fi

# Check if python3 is installed
if ! command -v python3 &> /dev/null; then
    echo "Error: python3 is not installed. Please install it before running this script."
    exit 1
fi

# Check if Flask is installed
if ! python3 -m flask --version &> /dev/null; then
    echo "Error: Flask is not installed. Please install it before running this script."
    exit 1
fi

These checks ensure that essential dependencies (sqlite3, python3, and Flask) are installed before proceeding with the database setup.

Initialization and Migration:

  • The initialization of the migration directory involves checking if it exists; if not, the script initializes it using the command python3 -m flask db init. This directory is crucial for managing database migrations and storing version information.

  • Handle backup/restoration based on the existence of the SQLite database.

# Check if the migration directory exists
if [ ! -d "migrations" ]; then
    echo "Initializing migration for the first time..."
    python3 -m flask db init
fi

# Check if sqlite.db does not exist and there is a sqlite-backup.db file
if [ ! -e "instance/volumes/sqlite.db" ] && [ -e "instance/volumes/sqlite-backup.db" ]; then
    echo "No sqlite.db found, using sqlite-backup.db to generate the file."
    
    # Copy backup file to primary (sqlite.db)
    cp "instance/volumes/sqlite-backup.db" "instance/volumes/sqlite.db"

    # Extract the new Alembic version from the backup database
    backup_version=$(sqlite3 instance/volumes/sqlite.db "SELECT version_num FROM alembic_version;")
    echo "Version ${backup_version} detected"

    python3 -m flask db stamp "${backup_version}"
fi

This section handles the initialization of the migration directory if it’s not present. It also checks for the existence of the SQLite database and, if necessary, restores from a backup, ensuring the database is in the correct state.

Database Migrations:

  • Running migrations involves executing the command python3 -m flask db migrate, which applies schema changes to the database. Subsequently, upgrading the database is achieved with python3 -m flask db upgrade, ensuring that the database structure is synchronized with the application’s schema.
# Perform database migrations
python3 -m flask db migrate

# Perform database upgrade
python3 -m flask db upgrade

# Run a custom command to generate data
python3 -m flask custom generate_data

These commands execute the database migrations to apply schema changes and then upgrade the database accordingly. Additionally, a custom command is run to generate initial data for the application.

Object Oriented Code (OOP)

  • Object-Oriented Programming (OOP) involves defining a User class that inherits from db.Model in the Flask-SQLAlchemy library. This class represents a User model with attributes such as id, username, and email. The use of classes and inheritance reflects key OOP principles, allowing developers to model entities in a structured and reusable manner. The db.Model provides a foundation for creating database models, and each instance of the User class corresponds to a record in the User table of the database.
# File: main/models.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    """Example User model."""
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

This defines a simple User model using Flask-SQLAlchemy, showcasing OOP principles. The User class represents a table in the database with columns for id, username, and email.

SQLite Tables

  • This snippet defines a simple User model using Flask-SQLAlchemy, showcasing OOP principles. The User class represents a table in the database with columns for id, username, and email.
# File: main/models.py

class User(db.Model):
    """Represents the User table in the SQLite database."""
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)

This showcases the definition of the User table, providing a clear representation of its structure through OOP principles. The use of db.Model facilitates the creation of database models, and each attribute corresponds to a column in the SQLite table.

Data Generation:

  • The custom_commands.py file introduces a custom command, GenerateDataCommand, responsible for populating the application’s database with initial data. Running the command python3 -m flask custom generate_data executes the data generation logic defined in the run method of the custom command, simulating the creation of user data for the User table.
# File: main/custom_commands.py

import random
import string
from flask import current_app
from flask_script import Command
from .models import db, User

fake = Faker()

class GenerateDataCommand(Command):
    """Custom command to generate data for the application."""
    
    def run(self):
        """Populates the database with initial data."""
        print("Generating data for the application...")

        # Generate and insert 10 fictional users
        for _ in range(10):
            username = self.generate_random_username()
            email = fake.email()
            password_hash = 'hashed_password'  # Use a secure password hashing method

            # Create a new user instance
            new_user = User(username=username, email=email, password_hash=password_hash)

            # Add the user to the database
            db.session.add(new_user)

        # Commit the changes to the database
        db.session.commit()

        print("Data generation completed.")

    def generate_random_username(self):
        """Generate a random username."""
        return ''.join(random.choice(string.ascii_letters) for _ in range(8)).lower()

# Register the custom command with Flask-Script
current_app.cli.add_command(GenerateDataCommand())
  • The GenerateDataCommand class generates 10 fictional users with random usernames, email addresses, and a placeholder for the hashed password.
  • Each user is created as a User instance and added to the SQLAlchemy session using db.session.add(new_user).
  • Finally, the changes are committed to the database with db.session.commit().