Build an IT Support Ticket Tracker

Build a tested Python and SQLite ticket tracker with persistent support records.

Introduction

30 Second Summary

Support requests become difficult to manage when incidents live in chat messages or memory. A recurring issue becomes tomorrow's mystery when nobody can see its history.

In this project, you will build an IT support ticket tracker as a command-line interface using Python plus SQLite. Its local database keeps every incident available after the program restarts.

What You'll Build

When you finish, you can reopen your local support desk to find every ticket waiting with its current status.

By the end of this project, you'll have:

  • A persistent ticket queue that keeps recorded incidents available across program restarts.
  • A complete support workflow that tracks each incident from creation to closure with live open-versus-closed totals.
  • A public GitHub repository that presents automated tests, technical documentation, and Git history as evidence of your engineering process.
  • Secret Mission: Add a tested priority filter that lets a support agent focus on high-priority tickets.

Are there any prerequisites?

You need basic Python syntax plus introductory SQL familiarity on a Windows computer with Visual Studio Code available. Step 1 confirms Python 3.8 or later plus SQLite availability before you write code.

Before We Start

This checkpoint helps you commit to what you are building before the hands-on work begins. You are creating a command-line tracker for support agents who need incident records to survive after the program closes.

Prepare and Verify Your Workspace

A build-from-scratch exercise only measures your engineering skills when the workspace is trustworthy. An unverified interpreter can make correct code look broken.

This step puts your project folder inside Visual Studio Code. Its integrated terminal keeps your commands focused on that folder.

Python runs the tracker through its built-in SQLite interface. Git records the history of your work.

In this step, get ready to:
  • Open the project workspace in Visual Studio Code.
  • Verify the development tools.
  • Initialize the starter repository with its two files.
Create the project workspace

Every file in the tracker needs one dependable home. Opening that folder as the workspace also points the integrated terminal at the right location.

  • Press the Windows key to open Windows search.
  • Type Visual Studio Code.
  • Press Enter to open the editor.
  • Click File in the top menu bar.
  • Click Open Folder....

You should now see the Windows folder picker.

  • Select your Desktop in the folder picker.
  • Use the folder picker control for creating a folder.
  • Enter it-support-ticket-tracker as the folder name.
  • Select the it-support-ticket-tracker folder.
  • Confirm the folder selection.

You should see it-support-ticket-tracker as the active folder in Visual Studio Code.

  • Click View in the top menu bar.
  • Click Terminal.

You should see the integrated terminal at the bottom of the editor. Its prompt should end inside it-support-ticket-tracker.

Verify Python, SQLite, and Git

The tracker requires Python 3.8 or later. The same interpreter must provide the sqlite3 module.

  • Check the installed Python version by running this command:
python --version

What does this check?

This command prints the version used by python in the integrated terminal. The result shows whether the interpreter meets the project minimum.

✔️ I see a supported version

Your interpreter meets the requirement when the version is 3.8 or later. Good work. Python is ready to run the tracker.

  • Continue to the remaining environment checks.

ⓧ I see an older version

The installed interpreter is below the project minimum. A current stable runtime gives the tracker the supported Python environment it needs.

  • Visit the official Python downloads page.
  • Install a current stable runtime through the Python Install Manager.
  • Approve the Windows installation confirmation if it appears.
  • Close the current integrated terminal using its panel controls.
  • Click View in the top menu bar.
  • Click Terminal to start a fresh terminal.
  • Repeat the version check by running:
python --version

What should change?

The fresh terminal should report the newly installed runtime. Continue when the version is 3.8 or later.

ⓧ Command not found

Windows cannot currently resolve the Python command. Installing a current stable runtime adds Python to your development environment.

  • Visit the official Python downloads page.
  • Install a current stable runtime through the Python Install Manager.
  • Approve the Windows installation confirmation if it appears.
  • Close the current integrated terminal using its panel controls.
  • Click View in the top menu bar.
  • Click Terminal to start a fresh terminal.
  • Check the installed runtime by running:
python --version

What should appear?

The command should now print a Python version. Continue when the version is 3.8 or later.

Python includes its SQLite interface in the standard library. The remaining checks confirm that interface is available from the same terminal.

  • Run the remaining environment checks with these commands:
python -c "import sqlite3; print(sqlite3.sqlite_version)"
git --version

What do these checks prove?

  • The first command imports sqlite3. It prints the SQLite engine version available to Python.
  • The second command prints the installed Git version. This confirms that Git is available in the integrated terminal.

✔️ I see both version lines

You should see one SQLite version line followed by one Git version line. Both tools are ready for the project.

ⓧ The SQLite check fails

A failed import means the current Python runtime cannot provide the required SQLite interface. Installing a current stable runtime restores the standard module.

  • Visit the official Python downloads page.
  • Install a current stable runtime through the Python Install Manager.
  • Close the current integrated terminal using its panel controls.
  • Click View in the top menu bar.
  • Click Terminal to start a fresh terminal.
  • Repeat the SQLite check by running:
python -c "import sqlite3; print(sqlite3.sqlite_version)"

What should appear?

You should see a SQLite version number. That output confirms Python can import the database module.

ⓧ The Git command is unavailable

The integrated terminal cannot currently resolve the Git command. The official installation guide provides the supported Windows setup path.

  • Visit the official Git installation guide.
  • Follow the Windows installation instructions.
  • Close the current integrated terminal using its panel controls.
  • Click View in the top menu bar.
  • Click Terminal to start a fresh terminal.
  • Repeat the Git check by running:
git --version

What should appear?

You should see a Git version line. That output confirms the fresh terminal can find Git.

Still missing a version?

  • Restart Visual Studio Code if the terminal still reports the result from before an installation.
  • Confirm the terminal prompt still points to it-support-ticket-tracker.

Need help with a failed check? Help me diagnose my Windows workspace verification.

Create the starter files and initialize Git

The .gitignore file keeps generated caches and local ticket data out of future commits. The empty README.md reserves a place for documentation later.

  • Select the new-file control in the left file sidebar.
  • Enter README.md as the filename.
  • Press Enter to create the file.
  • Select the new-file control in the left file sidebar again.
  • Enter .gitignore as the filename.
  • Press Enter to create the file.

You should see README.md plus .gitignore in the file sidebar.

  • Leave README.md empty.
  • Select .gitignore in the file sidebar.
  • Add the ignore rules by pasting this exact content:
__pycache__/
*.py[codz]
tickets.db

What do these rules exclude?

  • The __pycache__/ rule ignores Python's generated cache directory.
  • The *.py[codz] rule ignores compiled Python file variants.
  • The tickets.db rule keeps local ticket records out of version history.
  • Save .gitignore by pressing Ctrl+S.

The unsaved marker on the .gitignore tab should disappear.

Ignore rules look different?

  • Confirm the filename begins with a dot.
  • Confirm each ignore rule occupies its own line.

Need help checking the file? Help me compare my ignore rules with the required file.

  • Initialize the local repository by running this command:
git init

What does this command do?

Git creates repository metadata inside it-support-ticket-tracker. This prepares the folder to track future changes.

You should see a message confirming that Git initialized a repository in the project folder.

Before the final check, which two starter files do you expect Git to identify as untracked?

  • Verify the tools and repository state together by running:
python --version
python -c "import sqlite3; print(sqlite3.sqlite_version)"
git --version
git status

What does the final check cover?

  • The first command confirms the active Python runtime.
  • The second command confirms Python can load its SQLite interface.
  • The third command confirms Git is available.
  • The final command reports the repository state.

You should see Python 3.8 or later. You should also see SQLite plus Git version lines.

The repository report should list .gitignore plus README.md as untracked files. That result proves the starter files are inside the initialized repository.

That is the foundation in place. Your workspace can now support a Python tracker backed by SQLite and recorded with Git.

Starter files not listed?

  • Check that the terminal prompt points to it-support-ticket-tracker.
  • Confirm .gitignore appears in the left file sidebar.
  • Confirm README.md appears in the left file sidebar.
  • Save .gitignore before repeating the final check.

Still stuck? Help me find why Git cannot see my starter files.

✔️ Awesome, I've got everything!

Your two starter files are saved inside the initialized repository. The verified workspace is ready for the tracker code.

ⓧ I'd like to double check the full code

  • Compare your .gitignore with this complete reference file:
__pycache__/
*.py[codz]
tickets.db

What should match?

Your file should contain these three lines in this order. Each pattern matches a required ignore rule.

  • Confirm README.md contains no text.

The blank file is intentional. You will document the application after its behavior has been built and tested.

Your editor, interpreter, database module, and repository are ready. Next, you will build a working ticket tracker that deliberately forgets its records after closing.

Build a Tracker That Forgets

Your verified workspace gives you a clean starting point. Now you'll turn the requirements into a working command-line interface.

The first version keeps its tickets as in-memory state. A restart check will reveal how long that state lasts.

In this step, get ready to:
  • Define tickets with an ID, title, category, priority, and status.
  • Build a numbered menu that creates or displays tickets.
  • Restart the tracker to test whether its tickets survive.
Build the menu loop

The main() function owns the prompts that a support agent sees. Keeping those prompts together leaves the ticket operations available for independent testing later.

This first runnable slice covers the menu and its exit path. You will use that path for the first check.

  • In the Visual Studio Code Explorer sidebar, select the control for creating a file.
  • Type ticket_tracker.py as the file name.
  • Press Enter.
  • Add the menu loop by copying this code into ticket_tracker.py:
def main():
    tickets = []

    while True:
        print("\nIT Support Ticket Tracker")
        print("1. Create ticket")
        print("2. View tickets")
        print("3. Exit")
        choice = input("Choose an option: ").strip()

        if choice == "1":
            title = input("Title: ")
            category = input("Category: ")
            priority = input("Priority (low/medium/high): ").strip().lower()
            if priority not in VALID_PRIORITIES:
                print("Priority must be low, medium, or high.")
                continue
            ticket_id = create_ticket(tickets, title, category, priority)
            print(f"Created ticket {ticket_id}.")
        elif choice == "2":
            print_tickets(list_tickets(tickets))
        elif choice == "3":
            print("Goodbye.")
            break
        else:
            print("Choose a number from 1 to 3.")


if __name__ == "__main__":
    main()

What does this code do?

  • The tickets list holds ticket records for the current session.
  • The while loop keeps the menu available after each choice.
  • The numbered branches route each choice to the matching ticket operation.
  • The final condition starts main() when you run the file as a script.
  • Save ticket_tracker.py.
  • Switch back to the integrated terminal from earlier.
  • Start the script by running this command. The program pauses when the numbered menu asks for a choice:
python ticket_tracker.py

You should see the tracker title followed by three numbered choices.

  • Enter 3 to test the exit path.

You should see Goodbye. before the terminal prompt returns. Good start. Your tracker now launches into a usable menu.

Menu not appearing?

  • Confirm that you saved ticket_tracker.py before running it.
  • Confirm that the integrated terminal is still inside the it-support-ticket-tracker folder.
  • Choose only 3 during this first check because the ticket operations arrive in the next chunk.

Still stuck? Help me diagnose why ticket_tracker.py does not show its numbered menu.

Add the in-memory ticket operations

Each ticket needs the same five fields so every record has a predictable shape. Small data functions keep creation separate from the menu that collects input.

This slice unlocks the create path. The returned ticket ID gives you an immediate signal that a record entered the list.

  • Return to ticket_tracker.py in the editor.
  • Place your cursor above def main():.
  • Add the ticket structure and data operations by copying this code:
VALID_PRIORITIES = ("low", "medium", "high")


def create_ticket(tickets, title, category, priority):
    ticket = {
        "id": len(tickets) + 1,
        "title": title,
        "category": category,
        "priority": priority,
        "status": "open",
    }
    tickets.append(ticket)
    return ticket["id"]


def list_tickets(tickets):
    return tickets

How are tickets represented?

  • The VALID_PRIORITIES tuple defines the three accepted priority values.
  • Each ticket dictionary stores its ID, title, category, priority, and status.
  • The ticket ID uses the current list length to produce a new numeric value.
  • The list_tickets() function returns the records without handling any terminal output.
  • Save ticket_tracker.py.
  • Switch back to the integrated terminal from earlier.
  • Start the updated tracker by running:
python ticket_tracker.py
  • Enter 1 at the menu.
  • Enter Wi-Fi outage for the title.
  • Enter network for the category.
  • Enter high for the priority.

You should see Created ticket 1. before the menu returns. That is your first support incident stored in the current session.

  • Enter 3 to end this check.

Ticket not being created?

  • Confirm that VALID_PRIORITIES appears above def main():.
  • Enter low, medium, or high when the priority prompt appears.
  • Check that create_ticket() returns ticket["id"] after appending the ticket.

Need another pair of eyes? Help me find why my in-memory tracker does not create ticket 1.

Display tickets and test a restart

A support queue becomes useful when its records are readable. The display function handles an empty queue as well as a populated one.

  • Return to ticket_tracker.py in the editor.
  • Place your cursor below list_tickets().
  • Add the ticket display function by copying this code above def main()::
def print_tickets(tickets):
    if not tickets:
        print("No tickets found.")
        return

    print("\nID | Title | Category | Priority | Status")
    print("-" * 65)
    for ticket in tickets:
        print(
            f"{ticket['id']} | {ticket['title']} | {ticket['category']} | "
            f"{ticket['priority']} | {ticket['status']}"
        )

What does this function do?

  • The empty-list branch prints a clear message before returning from the function.
  • The header labels the five fields stored in every ticket.
  • The loop formats each ticket as one row that a support agent can scan.
  • Save ticket_tracker.py.
  • Switch back to the integrated terminal from earlier.
  • Start the complete in-memory tracker by running:
python ticket_tracker.py
  • Enter 1 at the menu.
  • Enter Wi-Fi outage for the title.
  • Enter network for the category.
  • Enter high for the priority.
  • Enter 2 when the menu returns.

You should see the ticket table followed by 1 | Wi-Fi outage | network | high | open. Your create and view flow now works from end to end.

  • Enter 3 to exit the first session.

Before you restart the program, do you think the ticket from the first session will still appear?

  • Start a second session by running the same command again:
python ticket_tracker.py
  • Enter 2 to view the queue.

You should see No tickets found. The missing ticket is the intended shortfall in this version.

  • Enter 3 to exit the second session.

Why did the ticket disappear?

The tickets = [] line creates a fresh list whenever main() starts. The old list disappears when the first Python process ends.

Your program can manage tickets during one session. Durable storage is the missing capability exposed by this restart test.

Seeing a different result?

  • Confirm that you entered 2 before exiting the first session so the ticket row was visible.
  • Confirm that you entered 3 to end the first process before starting the second process.
  • Check that tickets = [] is the first line inside main().

If your restart result still differs, help me trace the lifecycle of the tickets list in ticket_tracker.py.

✔️ Awesome, I've got everything!

Your tracker creates tickets, displays them during one session, and exposes the restart limitation.

ⓧ I'd like to double check the full code

  • Compare your saved ticket_tracker.py with this complete version:
VALID_PRIORITIES = ("low", "medium", "high")


def create_ticket(tickets, title, category, priority):
    ticket = {
        "id": len(tickets) + 1,
        "title": title,
        "category": category,
        "priority": priority,
        "status": "open",
    }
    tickets.append(ticket)
    return ticket["id"]


def list_tickets(tickets):
    return tickets


def print_tickets(tickets):
    if not tickets:
        print("No tickets found.")
        return

    print("\nID | Title | Category | Priority | Status")
    print("-" * 65)
    for ticket in tickets:
        print(
            f"{ticket['id']} | {ticket['title']} | {ticket['category']} | "
            f"{ticket['priority']} | {ticket['status']}"
        )


def main():
    tickets = []

    while True:
        print("\nIT Support Ticket Tracker")
        print("1. Create ticket")
        print("2. View tickets")
        print("3. Exit")
        choice = input("Choose an option: ").strip()

        if choice == "1":
            title = input("Title: ")
            category = input("Category: ")
            priority = input("Priority (low/medium/high): ").strip().lower()
            if priority not in VALID_PRIORITIES:
                print("Priority must be low, medium, or high.")
                continue
            ticket_id = create_ticket(tickets, title, category, priority)
            print(f"Created ticket {ticket_id}.")
        elif choice == "2":
            print_tickets(list_tickets(tickets))
        elif choice == "3":
            print("Goodbye.")
            break
        else:
            print("Choose a number from 1 to 3.")


if __name__ == "__main__":
    main()

Your tracker now handles a complete ticket session. Next, you'll use SQLite to make the queue survive each restart.

Make Every Ticket Survive a Restart

Your tracker already handles ticket creation through a working menu. The previous restart exposed the missing piece: every ticket lived only in memory.

A local SQLite database gives the queue storage that survives program exits. You will define the table before moving the data operations onto one managed connection.

In this step, get ready to:
  • Define a relational table for support tickets.
  • Replace the in-memory operations with parameterized SQL.
  • Control the database connection for the application lifetime.
Create the SQLite schema

A database schema defines the shape of each stored ticket. Constraints also prevent the database from accepting unsupported priorities or statuses.

  • In ticket_tracker.py, replace the opening constants section with the following database setup code:
import sqlite3

DATABASE_FILE = "tickets.db"
VALID_PRIORITIES = ("low", "medium", "high")


def initialize_database(connection):
    connection.execute(
        """
        CREATE TABLE IF NOT EXISTS tickets (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            category TEXT NOT NULL,
            priority TEXT NOT NULL CHECK (priority IN ('low', 'medium', 'high')),
            status TEXT NOT NULL DEFAULT 'open'
                CHECK (status IN ('open', 'closed'))
        )
        """
    )
    connection.commit()

What does this code do?

  • The sqlite3 module gives Python access to SQLite without a separate database server.
  • The DATABASE_FILE constant keeps the database filename in one place.
  • The initialize_database() function creates the tickets table when it does not already exist.
  • The database checks restrict priority to low, medium, or high.
  • The status constraint accepts only open or closed.
  • Save ticket_tracker.py.
  • Confirm the new database setup code loads by running this command in the terminal from earlier:
python ticket_tracker.py

What does this check prove?

Python loads the new import and schema function before starting the existing menu. Seeing the menu confirms the database setup code has valid syntax.

  • Enter 3 to exit the tracker.

You should see the familiar menu followed by Goodbye..

Does the menu fail to load?

  • Check that import sqlite3 is the first line in ticket_tracker.py.
  • Compare the indentation inside initialize_database() with the code block above.

Still stuck? Help me find the syntax problem in my SQLite schema function.

Move ticket operations into SQLite

The tracker now needs data functions that talk to the database connection. Parameterized SQL keeps ticket values separate from the SQL statement structure.

  • In ticket_tracker.py, replace the existing create_ticket() and list_tickets() functions with this query layer:
def create_ticket(connection, title, category, priority):
    cursor = connection.execute(
        """
        INSERT INTO tickets (title, category, priority, status)
        VALUES (?, ?, ?, 'open')
        """,
        (title, category, priority),
    )
    connection.commit()
    return cursor.lastrowid


def list_tickets(connection):
    cursor = connection.execute(
        """
        SELECT id, title, category, priority, status
        FROM tickets
        ORDER BY id
        """
    )
    return cursor.fetchall()

How does the query layer work?

  • The INSERT statement stores one ticket with an open status.
  • The ? placeholders bind the submitted title, category, and priority as values.
  • The commit writes the new row to tickets.db.
  • The SELECT statement returns every ticket in numeric ID order.
  • The fetchall() call returns ordinary tuples for the display function.

The application needs one connection shared by every menu operation. The next two code blocks form one complete main() function.

  • Replace the existing main() function with both code blocks below before saving the file:
def main():
    connection = sqlite3.connect(DATABASE_FILE)
    initialize_database(connection)

    try:
        while True:
            print("\nIT Support Ticket Tracker")
            print("1. Create ticket")
            print("2. View tickets")
            print("3. Exit")
            choice = input("Choose an option: ").strip()

            if choice == "1":
                title = input("Title: ")
                category = input("Category: ")
                priority = input("Priority (low/medium/high): ").strip().lower()
                if priority not in VALID_PRIORITIES:
                    print("Priority must be low, medium, or high.")
                    continue
                ticket_id = create_ticket(connection, title, category, priority)
                print(f"Created ticket {ticket_id}.")
            elif choice == "2":
                print_tickets(list_tickets(connection))

What happens when the tracker starts?

  • The connect() call opens tickets.db through the shared connection variable.
  • The schema initialization creates the tickets table during the first run.
  • The create and view choices now pass the database connection into the data functions.
  • Complete main() by pasting this continuation directly below the previous block:
            elif choice == "3":
                print("Goodbye.")
                break
            else:
                print("Choose a number from 1 to 3.")
    finally:
        connection.close()

Why use finally?

The finally block closes the database connection whenever main() finishes. This cleanup still runs when an unexpected error interrupts the menu.

  • Save ticket_tracker.py.
  • Start the SQLite-backed tracker by running:
python ticket_tracker.py

What changes on this run?

The command opens one connection to tickets.db before the menu starts. The schema function prepares the table for the first stored ticket.

  • Enter 1 to create a ticket.
  • Enter Wi-Fi outage for the title.
  • Enter network for the category.
  • Enter high for the priority.

You should see Created ticket 1. in the terminal.

  • Enter 3 to close the tracker.
  • Switch back to the Explorer sidebar.

You should see tickets.db inside it-support-ticket-tracker. You have crossed the persistence boundary: the ticket now exists outside Python's process memory.

Missing the database or ticket ID?

  • Confirm the integrated terminal is still inside the it-support-ticket-tracker folder.
  • Check that initialize_database(connection) runs immediately after the connection opens.
  • Check that create_ticket() calls connection.commit() before returning the ID.

Need another pair of eyes? Help me debug why tickets.db or my first stored ticket is missing.

Prove tickets survive a restart

SQLite query rows arrive as tuples in the same order as the selected columns. The display loop must unpack those values before printing the stored ticket.

  • In ticket_tracker.py, replace the existing print_tickets() function with this tuple-based version:
def print_tickets(tickets):
    if not tickets:
        print("No tickets found.")
        return

    print("\nID | Title | Category | Priority | Status")
    print("-" * 65)
    for ticket_id, title, category, priority, status in tickets:
        print(f"{ticket_id} | {title} | {category} | {priority} | {status}")

How does tuple unpacking help?

Each row contains the five selected columns in a fixed order. The loop assigns those values readable names before formatting one ticket row.

  • Save ticket_tracker.py.

Before you restart the tracker, make a prediction about whether the Wi-Fi outage ticket will still appear.

  • Restart the tracker by running:
python ticket_tracker.py

What happens on restart?

The tracker reconnects to the existing tickets.db file. The table initialization leaves the existing table intact.

  • Enter 2 to view the stored tickets.

You should see the row 1 | Wi-Fi outage | network | high | open. That row proves the ticket survived the first program exit.

  • Enter 3 to exit the tracker.

Does the restarted queue look empty?

  • Confirm DATABASE_FILE is set to tickets.db.
  • Confirm both runs started from the terminal inside it-support-ticket-tracker.
  • Compare the INSERT and SELECT column order with the final code below.

Still seeing an empty queue? Help me trace why my SQLite ticket disappears after restarting the program.

✔️ Awesome, I've got everything!

Your saved ticket reappears after restarting the tracker. Make sure ticket_tracker.py is saved before moving on.

ⓧ I'd like to double check the full code

Compare your ticket_tracker.py with this complete SQLite-backed version.

import sqlite3

DATABASE_FILE = "tickets.db"
VALID_PRIORITIES = ("low", "medium", "high")


def initialize_database(connection):
    connection.execute(
        """
        CREATE TABLE IF NOT EXISTS tickets (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            category TEXT NOT NULL,
            priority TEXT NOT NULL CHECK (priority IN ('low', 'medium', 'high')),
            status TEXT NOT NULL DEFAULT 'open'
                CHECK (status IN ('open', 'closed'))
        )
        """
    )
    connection.commit()


def create_ticket(connection, title, category, priority):
    cursor = connection.execute(
        """
        INSERT INTO tickets (title, category, priority, status)
        VALUES (?, ?, ?, 'open')
        """,
        (title, category, priority),
    )
    connection.commit()
    return cursor.lastrowid


def list_tickets(connection):
    cursor = connection.execute(
        """
        SELECT id, title, category, priority, status
        FROM tickets
        ORDER BY id
        """
    )
    return cursor.fetchall()


def print_tickets(tickets):
    if not tickets:
        print("No tickets found.")
        return

    print("\nID | Title | Category | Priority | Status")
    print("-" * 65)
    for ticket_id, title, category, priority, status in tickets:
        print(f"{ticket_id} | {title} | {category} | {priority} | {status}")


def main():
    connection = sqlite3.connect(DATABASE_FILE)
    initialize_database(connection)

    try:
        while True:
            print("\nIT Support Ticket Tracker")
            print("1. Create ticket")
            print("2. View tickets")
            print("3. Exit")
            choice = input("Choose an option: ").strip()

            if choice == "1":
                title = input("Title: ")
                category = input("Category: ")
                priority = input("Priority (low/medium/high): ").strip().lower()
                if priority not in VALID_PRIORITIES:
                    print("Priority must be low, medium, or high.")
                    continue
                ticket_id = create_ticket(connection, title, category, priority)
                print(f"Created ticket {ticket_id}.")
            elif choice == "2":
                print_tickets(list_tickets(connection))
            elif choice == "3":
                print("Goodbye.")
                break
            else:
                print("Choose a number from 1 to 3.")
    finally:
        connection.close()


if __name__ == "__main__":
    main()

How should you use this reference?

Check the imports, functions, indentation, and menu flow in order. Your file should match this reference exactly.

Your ticket queue now survives a restart through a managed SQLite connection. Next, you will add validation, status changes, summaries, and automated tests.

Make It Reliable and Publish the Evidence

The SQLite tracker now keeps support tickets across restarts. Reliable storage still needs input safeguards plus a controlled way to update each ticket.

Manual demonstrations can miss invalid values or regressions. You will add validation plus status operations. You will use unit testing to prove the behavior before publishing the evidence.

In this step, get ready to:
  • Validate ticket data before storing it.
  • Test database behavior with isolated automated tests.
  • Document the project before publishing it with Git and GitHub.
Validate input and add operations

Validation inside create_ticket() gives the command-line interface clear feedback before an invalid ticket reaches the database. The interface needs to catch each validation failure so the menu can keep running.

  • In ticket_tracker.py, find the existing choice == "1" branch.
  • Replace that branch with this error-handling flow:
            if choice == "1":
                title = input("Title: ")
                category = input("Category: ")
                priority = input("Priority (low/medium/high): ")
                try:
                    ticket_id = create_ticket(connection, title, category, priority)
                    print(f"Created ticket {ticket_id}.")
                except ValueError as error:
                    print(f"Could not create ticket: {error}")

How does the interface handle validation?

  • The try block sends the submitted fields to create_ticket().
  • The except block converts a ValueError into readable feedback.
  • The menu continues after displaying the feedback.
  • Save ticket_tracker.py.
  • Confirm the updated branch still creates a valid ticket by running:
python ticket_tracker.py

What does this run check?

This run checks that the new error-handling structure preserves the successful creation path. Your existing tickets.db file remains connected to the tracker.

  • Enter 1 at the menu.
  • Enter Keyboard replacement at the title prompt.
  • Enter hardware at the category prompt.
  • Enter low at the priority prompt.
  • Enter 3 to exit the current menu.

You should see a created ticket ID followed by Goodbye. when you exit. The successful path still works after the interface change.

  • In ticket_tracker.py, replace the existing create_ticket() function with this validated version:
def create_ticket(connection, title, category, priority):
    title = title.strip()
    category = category.strip()
    priority = priority.strip().lower()

    if not title:
        raise ValueError("Title cannot be blank.")
    if not category:
        raise ValueError("Category cannot be blank.")
    if priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    cursor = connection.execute(
        """
        INSERT INTO tickets (title, category, priority, status)
        VALUES (?, ?, ?, 'open')
        """,
        (title, category, priority),
    )
    connection.commit()
    return cursor.lastrowid

What does this code do?

  • The first three assignments normalize the submitted values before checking them.
  • Each ValueError identifies one failed ticket requirement.
  • The parameterized INSERT runs only after every value passes validation.
  • Save ticket_tracker.py.

Before you test the validation, what do you think the tracker will do with a blank title?

  • Start the tracker by running:
python ticket_tracker.py

What does this run check?

This run sends an incomplete ticket through the new validation layer. The menu should report the failed requirement without stopping the program.

  • Enter 1 at the menu.
  • Press Enter at the title prompt without typing a value.
  • Enter network at the category prompt.
  • Enter high at the priority prompt.
  • Enter 3 to exit the current menu.

You should see Could not create ticket: Title cannot be blank. without a traceback. Your tracker now stops incomplete tickets before they reach SQLite.

Did the tracker stop with a traceback?

  • Check that the validation statements sit inside create_ticket().
  • Check that the call to create_ticket() sits inside the try block.
  • Check that the except ValueError block has the same indentation as the try block.

Still stuck? Help me debug the ticket validation flow.

A support queue also needs a controlled status transition. A summary turns those ticket statuses into a count of current work plus completed work.

  • Add close_ticket() below list_tickets() in ticket_tracker.py:
def close_ticket(connection, ticket_id):
    cursor = connection.execute(
        """
        UPDATE tickets
        SET status = 'closed'
        WHERE id = ?
        """,
        (ticket_id,),
    )
    connection.commit()
    return cursor.rowcount == 1

How does ticket closing work?

  • The WHERE id = ? condition limits the update to the requested ticket.
  • The parameter tuple binds the numeric ID to the placeholder.
  • The return value reports whether one row changed.
  • Add get_summary() below close_ticket() in ticket_tracker.py:
def get_summary(connection):
    cursor = connection.execute(
        """
        SELECT status, COUNT(*)
        FROM tickets
        GROUP BY status
        """
    )
    summary = {"open": 0, "closed": 0}
    for status, count in cursor.fetchall():
        summary[status] = count
    return summary

How is the summary calculated?

  • The query groups tickets by their current status.
  • The COUNT(*) expression counts the tickets in each group.
  • The initial dictionary preserves a zero for any status without rows.
  • Save ticket_tracker.py.
  • Confirm the existing menu still starts after adding both functions by running:
python ticket_tracker.py

What does this run prove?

This run checks that both new function definitions load without a syntax error. The menu remains usable while you prepare its new options.

  • Enter 3 to exit the current menu.

You should see Goodbye. after the tracker starts normally. Both new database operations are now available for the menu to call.

  • In main(), replace the current menu display with these five options:
        while True:
            print("\nIT Support Ticket Tracker")
            print("1. Create ticket")
            print("2. View tickets")
            print("3. Close ticket")
            print("4. View summary")
            print("5. Exit")
            choice = input("Choose an option: ").strip()

What changes in the menu?

  • Option 3 now starts the status transition.
  • Option 4 displays the queue summary.
  • Option 5 becomes the exit choice.
  • Replace the current option 3 branch through the final invalid-choice branch with this flow:
            elif choice == "3":
                try:
                    ticket_id = int(input("Ticket ID to close: "))
                    if close_ticket(connection, ticket_id):
                        print(f"Closed ticket {ticket_id}.")
                    else:
                        print("No ticket has that ID.")
                except ValueError:
                    print("Ticket ID must be a whole number.")
            elif choice == "4":
                summary = get_summary(connection)
                print(f"Open: {summary['open']} | Closed: {summary['closed']}")
            elif choice == "5":
                print("Goodbye.")
                break
            else:
                print("Choose a number from 1 to 5.")

How do the new choices behave?

  • Option 3 converts the submitted ID to an integer before closing the ticket.
  • Option 4 prints the current open plus closed totals.
  • Option 5 ends the menu loop.
  • Save ticket_tracker.py.

Before you run the completed tracker, what should happen to the totals after you close one open ticket?

  • Start the completed tracker by running:
python ticket_tracker.py

What are you verifying?

This run checks the complete command-line workflow against the persistent database. You will create one known ticket before changing its status.

  • Enter 1 at the menu.
  • Enter Reset student password at the title prompt.
  • Enter account at the category prompt.
  • Enter medium at the priority prompt.
  • Record the created ticket ID here: your created ticket ID.
  • Enter 4 to view the first summary.
  • Enter 3 to choose the close operation.
  • Enter your created ticket ID at the ticket ID prompt.
  • Enter 4 to view the updated summary.
  • Enter 5 to exit the tracker.

You should see the open count decrease by one. You should see the closed count increase by one.

Did the summary stay the same?

  • Check that close_ticket() calls connection.commit() after the update.
  • Check that option 3 passes the submitted ID to close_ticket().
  • Check that option 4 calls get_summary() after the update.

Need another pair of eyes? Help me debug why closing a ticket does not change the summary.

✔️ Awesome, I've got everything!

Your tracker now rejects invalid fields. It can close tickets plus report the current queue totals.

ⓧ I'd like to double check the full code

Compare your saved ticket_tracker.py with this complete version.

import sqlite3

DATABASE_FILE = "tickets.db"
VALID_PRIORITIES = ("low", "medium", "high")


def initialize_database(connection):
    connection.execute(
        """
        CREATE TABLE IF NOT EXISTS tickets (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            category TEXT NOT NULL,
            priority TEXT NOT NULL CHECK (priority IN ('low', 'medium', 'high')),
            status TEXT NOT NULL DEFAULT 'open'
                CHECK (status IN ('open', 'closed'))
        )
        """
    )
    connection.commit()


def create_ticket(connection, title, category, priority):
    title = title.strip()
    category = category.strip()
    priority = priority.strip().lower()

    if not title:
        raise ValueError("Title cannot be blank.")
    if not category:
        raise ValueError("Category cannot be blank.")
    if priority not in VALID_PRIORITIES:
        raise ValueError("Priority must be low, medium, or high.")

    cursor = connection.execute(
        """
        INSERT INTO tickets (title, category, priority, status)
        VALUES (?, ?, ?, 'open')
        """,
        (title, category, priority),
    )
    connection.commit()
    return cursor.lastrowid


def list_tickets(connection):
    cursor = connection.execute(
        """
        SELECT id, title, category, priority, status
        FROM tickets
        ORDER BY id
        """
    )
    return cursor.fetchall()


def close_ticket(connection, ticket_id):
    cursor = connection.execute(
        """
        UPDATE tickets
        SET status = 'closed'
        WHERE id = ?
        """,
        (ticket_id,),
    )
    connection.commit()
    return cursor.rowcount == 1


def get_summary(connection):
    cursor = connection.execute(
        """
        SELECT status, COUNT(*)
        FROM tickets
        GROUP BY status
        """
    )
    summary = {"open": 0, "closed": 0}
    for status, count in cursor.fetchall():
        summary[status] = count
    return summary


def print_tickets(tickets):
    if not tickets:
        print("No tickets found.")
        return

    print("\nID | Title | Category | Priority | Status")
    print("-" * 65)
    for ticket_id, title, category, priority, status in tickets:
        print(f"{ticket_id} | {title} | {category} | {priority} | {status}")


def main():
    connection = sqlite3.connect(DATABASE_FILE)
    initialize_database(connection)

    try:
        while True:
            print("\nIT Support Ticket Tracker")
            print("1. Create ticket")
            print("2. View tickets")
            print("3. Close ticket")
            print("4. View summary")
            print("5. Exit")
            choice = input("Choose an option: ").strip()

            if choice == "1":
                title = input("Title: ")
                category = input("Category: ")
                priority = input("Priority (low/medium/high): ")
                try:
                    ticket_id = create_ticket(connection, title, category, priority)
                    print(f"Created ticket {ticket_id}.")
                except ValueError as error:
                    print(f"Could not create ticket: {error}")
            elif choice == "2":
                print_tickets(list_tickets(connection))
            elif choice == "3":
                try:
                    ticket_id = int(input("Ticket ID to close: "))
                    if close_ticket(connection, ticket_id):
                        print(f"Closed ticket {ticket_id}.")
                    else:
                        print("No ticket has that ID.")
                except ValueError:
                    print("Ticket ID must be a whole number.")
            elif choice == "4":
                summary = get_summary(connection)
                print(f"Open: {summary['open']} | Closed: {summary['closed']}")
            elif choice == "5":
                print("Goodbye.")
                break
            else:
                print("Choose a number from 1 to 5.")
    finally:
        connection.close()


if __name__ == "__main__":
    main()

How should the file compare?

Confirm that the functions appear in the same order. Pay close attention to the five-option menu near the bottom.

Prove behavior with automated tests

Automated tests repeat important checks without changing your real tickets.db file. A fresh in-memory database gives every test an isolated starting point.

  • In the Visual Studio Code Explorer sidebar for it-support-ticket-tracker, create a file named test_ticket_tracker.py.
  • Add the imports plus the first test by copying this code into test_ticket_tracker.py:
import sqlite3
import unittest

from ticket_tracker import (
    close_ticket,
    create_ticket,
    get_summary,
    initialize_database,
    list_tickets,
)


class TicketTrackerTests(unittest.TestCase):
    def setUp(self):
        self.connection = sqlite3.connect(":memory:")
        initialize_database(self.connection)

    def tearDown(self):
        self.connection.close()

    def test_create_and_list_ticket(self):
        create_ticket(self.connection, "Wi-Fi outage", "network", "High")

        tickets = list_tickets(self.connection)

        self.assertEqual(len(tickets), 1)
        self.assertEqual(tickets[0][1], "Wi-Fi outage")
        self.assertEqual(tickets[0][3], "high")
        self.assertEqual(tickets[0][4], "open")

How is this test isolated?

  • The imports expose the tracker functions directly to the test file.
  • The setUp() method creates a fresh database before each test.
  • The tearDown() method closes that test connection.
  • The first test checks the stored title plus the normalized priority. It also checks the initial status.
  • Save test_ticket_tracker.py.

Before you run the first test, do you think it will change the tickets stored in tickets.db?

  • Run test discovery with this command:
python -m unittest -v

What does this command do?

The command asks Python to execute the unittest module. The -v option displays each discovered test by name.

You should see one test finish with OK. Your persistent tickets remain untouched because the test uses :memory:.

Was no test discovered?

  • Check that the file is named test_ticket_tracker.py.
  • Check that the class inherits from unittest.TestCase.
  • Check that the test method starts with test_.

Need help with discovery? Help me find why unittest cannot discover my ticket tracker test.

  • Add this invalid-priority test below test_create_and_list_ticket() inside the same class:
    def test_invalid_priority_is_rejected(self):
        with self.assertRaises(ValueError):
            create_ticket(self.connection, "Printer jam", "hardware", "urgent")

What does this test prove?

The test expects create_ticket() to raise ValueError for an unsupported priority. A missing exception makes the test fail.

  • Save test_ticket_tracker.py.
  • Run the two tests with this command:
python -m unittest -v

What does this run confirm?

The suite now checks successful creation plus rejected input. You should see two test names followed by OK.

  • Add the close-and-summary test below test_invalid_priority_is_rejected() inside the same class:
    def test_close_ticket_updates_summary(self):
        ticket_id = create_ticket(
            self.connection,
            "Reset student password",
            "account",
            "medium",
        )

        changed = close_ticket(self.connection, ticket_id)
        summary = get_summary(self.connection)

        self.assertTrue(changed)
        self.assertEqual(summary, {"open": 0, "closed": 1})


if __name__ == "__main__":
    unittest.main()

What does the final test cover?

  • The test creates one open ticket in its isolated database.
  • The call to close_ticket() changes that ticket's status.
  • The final assertion checks the exact summary after the transition.
  • Save test_ticket_tracker.py.

Before you run the complete suite, do you think all three tests begin with the same database state?

  • Run the complete test suite with this command:
python -m unittest -v

What proves the suite passed?

Verbose output lists all three test methods. The final OK confirms that the tested operations match the requirements.

You should see three tests finish with OK. That is repeatable evidence that creation, validation, closing, and summaries work.

Is one test failing?

  • Read the failing test name near the top of its traceback.
  • Compare the reported expected value with the actual value.
  • Check that setUp() creates a new :memory: connection for every test.

Want help reading the failure? Help me diagnose my failing ticket tracker test.

✔️ Awesome, I've got everything!

Your test suite now verifies three core behaviors against a fresh database every time.

ⓧ I'd like to double check the full code

Compare your saved test_ticket_tracker.py with this complete test file.

import sqlite3
import unittest

from ticket_tracker import (
    close_ticket,
    create_ticket,
    get_summary,
    initialize_database,
    list_tickets,
)


class TicketTrackerTests(unittest.TestCase):
    def setUp(self):
        self.connection = sqlite3.connect(":memory:")
        initialize_database(self.connection)

    def tearDown(self):
        self.connection.close()

    def test_create_and_list_ticket(self):
        create_ticket(self.connection, "Wi-Fi outage", "network", "High")

        tickets = list_tickets(self.connection)

        self.assertEqual(len(tickets), 1)
        self.assertEqual(tickets[0][1], "Wi-Fi outage")
        self.assertEqual(tickets[0][3], "high")
        self.assertEqual(tickets[0][4], "open")

    def test_invalid_priority_is_rejected(self):
        with self.assertRaises(ValueError):
            create_ticket(self.connection, "Printer jam", "hardware", "urgent")

    def test_close_ticket_updates_summary(self):
        ticket_id = create_ticket(
            self.connection,
            "Reset student password",
            "account",
            "medium",
        )

        changed = close_ticket(self.connection, ticket_id)
        summary = get_summary(self.connection)

        self.assertTrue(changed)
        self.assertEqual(summary, {"open": 0, "closed": 1})


if __name__ == "__main__":
    unittest.main()

How should the tests compare?

Confirm that all three test methods remain inside TicketTrackerTests. Confirm that setUp() plus tearDown() use the same connection attribute.

Document and publish the repository

A repository should explain the project before someone reads the implementation. Your README.md will record the purpose, features, data model, commands, and design choices.

  • Return to the empty README.md file from earlier.
  • Add the project overview plus the data model by copying this content:
# IT Support Ticket Tracker

A small command-line support desk built with Python and SQLite. It demonstrates requirements analysis, functional decomposition, relational persistence, input validation, automated testing, and version control.

## Features

- Create tickets with a title, category, and priority
- Preserve tickets in a local SQLite database
- List every recorded ticket
- Close a ticket by ID
- Display open and closed ticket totals
- Reject blank fields and unsupported priorities
- Verify core behavior with automated tests

## Data model

The `tickets` table stores an integer `id` plus text fields for `title`, `category`, `priority`, and `status`. Priority is limited to `low`, `medium`, or `high`. Status is limited to `open` or `closed`.

What does this documentation establish?

The opening identifies the project's purpose. The feature list plus data model show what the tracker supports.

  • Save README.md.
  • Open the Markdown preview from the Visual Studio Code editor toolbar.

You should see the project title followed by the Features section. The list should render as seven separate bullet points.

  • Add the run instructions plus the design choices below the data model:
## Run

```powershell
python ticket_tracker.py
```

## Test

```powershell
python -m unittest -v
```

## Design choices

- The command-line interface keeps the first version focused on programming and data flow rather than web-framework setup.
- SQLite provides relational persistence without a separate database server.
- Data operations accept a connection so production code can use `tickets.db` while tests use an isolated in-memory database.
- SQL values use placeholders rather than string formatting.

Why record these choices?

The command sections make the project reproducible. The design notes connect implementation decisions to the project's goals.

  • Save README.md.
  • Return to the open Markdown preview.

You should see separate Run plus Test command blocks. The document should end with four design choices.

✔️ Awesome, I've got everything!

Your README now explains how to run, test, and evaluate the tracker.

ⓧ I'd like to double check the full code

Compare your saved README.md with this complete document.

# IT Support Ticket Tracker

A small command-line support desk built with Python and SQLite. It demonstrates requirements analysis, functional decomposition, relational persistence, input validation, automated testing, and version control.

## Features

- Create tickets with a title, category, and priority
- Preserve tickets in a local SQLite database
- List every recorded ticket
- Close a ticket by ID
- Display open and closed ticket totals
- Reject blank fields and unsupported priorities
- Verify core behavior with automated tests

## Data model

The `tickets` table stores an integer `id` plus text fields for `title`, `category`, `priority`, and `status`. Priority is limited to `low`, `medium`, or `high`. Status is limited to `open` or `closed`.

## Run

```powershell
python ticket_tracker.py
```

## Test

```powershell
python -m unittest -v
```

## Design choices

- The command-line interface keeps the first version focused on programming and data flow rather than web-framework setup.
- SQLite provides relational persistence without a separate database server.
- Data operations accept a connection so production code can use `tickets.db` while tests use an isolated in-memory database.
- SQL values use placeholders rather than string formatting.

What should the final document contain?

Check for the title plus four main sections. Confirm that both PowerShell command blocks match the commands you used.

The local repository can now capture the implementation plus its evidence in one history entry. Your existing .gitignore keeps the local database outside that history.

  • Switch back to the Visual Studio Code terminal.
  • Review the repository state by running:
git status

What does this status check show?

Git reports the files that changed since the repository was initialized. The ignored tickets.db file should stay outside the files prepared for publication.

You should see ticket_tracker.py plus README.md as changed files. You should see test_ticket_tracker.py as an untracked file.

  • Stage the project files by running:
git add .

What does staging do?

The command adds the current project content to Git's index. That index supplies the files for the next commit.

  • Create the project commit by running:
git commit -m "Build tested ticket tracker"

What does this commit capture?

The commit records the tracker, tests, documentation, and ignore rules as one verified milestone. The terminal should report a new commit with changed files.

Did Git refuse the commit?

  • Read the terminal message to identify whether Git needs author information.
  • Complete the requested Git identity setup.
  • Repeat the commit command after the identity is configured.

Need help with the message? Help me understand why my Git commit was rejected.

The remote repository gives your work a public home. Create it without starter files so your existing local history can push cleanly.

  • Open GitHub in your browser.
  • Sign in to your GitHub account.
  • Select New repository.
  • Enter it-support-ticket-tracker in the repository name field.
  • Select Public as the visibility.
  • Leave the repository initialization options empty.
  • Select Create repository.

You should see an empty repository page with setup commands. That page confirms the public destination exists without conflicting starter files.

  • Replace OWNER with your GitHub username in the command below.
  • Connect the local repository to GitHub by running the edited command:
git remote add origin https://github.com/OWNER/it-support-ticket-tracker.git

What does this remote command do?

The command saves the GitHub repository URL under the remote name origin. Future pushes can reference that short name.

The first push may trigger an authentication prompt. Complete the supported browser sign-in or HTTPS authentication flow if GitHub requests it.

Before you push, which four project files do you expect to see on the repository page?

  • Push the current branch plus configure its upstream by running:
git push --set-upstream origin HEAD

What does the first push establish?

The command sends the current branch to origin. It configures that remote branch as the upstream for future pushes.

  • Complete the supported authentication flow if GitHub requests it.
  • Return to the public it-support-ticket-tracker repository page after the push finishes.
  • Refresh the repository page.

You should see ticket_tracker.py, test_ticket_tracker.py, .gitignore, and README.md on the default branch. Your tested tracker now has a public engineering record.

Did the push fail?

  • Check that you replaced OWNER with your exact GitHub username.
  • Check that the GitHub repository is named it-support-ticket-tracker.
  • Complete the supported HTTPS authentication flow if GitHub requests credentials.

Still blocked? Help me troubleshoot my first GitHub push.

Secret mission

Filter the Queue by Priority

A busy support queue becomes easier to manage when agents can focus on urgent tickets. Extend the tracker with an optional priority filter while preserving the existing all-ticket view.

Clean Up Your Resources

Clean Up Your Resources

Your tracker has no running service or ongoing project charge. Choose whether to keep the files, pause your work, or delete every resource.

Resources you used:

  • The local it-support-ticket-tracker folder with its source files plus documentation.
  • The local Git repository stored inside it-support-ticket-tracker.
  • The tickets.db file, which contains the manual test tickets in a local SQLite database.
  • The public GitHub repository at https://github.com/OWNER/it-support-ticket-tracker.git.

Keep everything running

No action needed. Choose this if you want to keep extending the ticket tracker.

  • Leave the it-support-ticket-tracker folder in place to preserve your source files.
  • Keep the local Git history available for future commits.
  • Leave tickets.db in the project folder to preserve your manual test tickets.
  • Keep the public GitHub repository available for sharing.

No application process needs to remain running.

Pause - I'll come back to this later

End your work session while keeping every file available for later.

  • Close Visual Studio Code.
  • Leave the it-support-ticket-tracker folder on your computer.
  • Leave the public GitHub repository in place.

Your Git history remains available. Your SQLite records remain in tickets.db.

Delete - I don't want to use this again

Remove all project resources to start fresh. Deletion is permanent, so confirm that you no longer need the local tickets or repository history.

  • Close Visual Studio Code.
  • Use the Windows file manager to locate the folder named it-support-ticket-tracker.
  • Right-click the it-support-ticket-tracker folder.
  • Choose the deletion option.
  • Search for it-support-ticket-tracker again to verify the local folder is gone.

You should no longer find the local folder. This also removes its Git history plus the tickets.db database.

The local copy is gone. The public repository still needs its own deletion.

  • Open the it-support-ticket-tracker GitHub repository.
  • Select Settings from the repository navigation.
  • Select General from the settings sidebar.
  • Scroll to Danger Zone.
  • Select Delete this repository.
  • Complete the confirmation prompts.
  • Reload the repository URL to verify the remote repository is gone.

You should see that the repository is unavailable. Your local project plus its remote copy are now removed.

Nice Work!

Nice Work!

You did it! Your tested Python and SQLite support desk now turns ticket requirements into a persistent workflow.

You've learned how to:

  • Built a command-line interface from stated support-desk requirements. The workflow covers each ticket from creation through closure. Its summary shows live open-versus-closed totals.
  • Replaced temporary list storage with a relational database. SQLite preserves ticket records across restarts. Database constraints protect priority and status values. Parameterized SQL binds user input through placeholders.
  • Created a public evidence trail for the project. Automated testing proves the database behavior across four isolated tests. Git records the implementation history. GitHub presents the code with its usage instructions and design choices.
  • Completed the Secret Mission by adding a tested priority filter. The queue can now isolate low, medium, or high tickets.

Ready to quiz yourself?