project-init

This commit is contained in:
srikar committed 2026-09-25 08:14:04 -07:00
commit 65691a0aee
7 files changed
+897

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
# Student Management System
A terminal CRUD application for managing **Students**, **Guardians**, and
**Attendance** records, backed by MySQL. Built against the schema in
Tables 9.3–9.5 (STUDENT, GUARDIAN, ATTENDANCE).
## What it does on startup
1. Connects to the MySQL server at `10.0.1.40:3306`.
2. Verifies the `student_db` database exists — creates it if not.
3. Verifies the `GUARDIAN`, `STUDENT`, and `ATTENDANCE` tables exist
(in that order, since STUDENT references GUARDIAN and ATTENDANCE
references STUDENT) — creates any that are missing, with the exact
data types and constraints from the schema (primary keys, foreign
keys, `NOT NULL`, `UNIQUE` on `GPhone`, and the composite primary key
`(AttendanceDate, RollNumber)` on ATTENDANCE).
4. Launches an interactive menu for Create / Read / Update / Delete.
## Setup
1. Install the one dependency:
```bash
pip install -r requirements.txt
```
2. Set your MySQL credentials as environment variables (the host/port/
database default to the assignment's values already):
```bash
export DB_USER=root
export DB_PASSWORD=your_password_here
# optional overrides — these already default correctly:
# export DB_HOST=10.0.1.40
# export DB_PORT=3306
# export DB_NAME=student_db
```
3. Run it:
```bash
python3 main.py
```
## Files
| File | Purpose |
|-----------------|------------------------------------------------------------------|
| `config.py` | Connection settings (reads env vars, falls back to defaults) |
| `db.py` | Connects to MySQL; verifies/creates the database and tables |
| `crud.py` | All Create/Read/Update/Delete operations for the three tables |
| `ui_helpers.py` | Input prompts/validation and table-printing for the terminal |
| `main.py` | The menu-driven application — run this |
## Using the app
The main menu has three sections:
- **Guardian management** — list, view, add, update, delete guardians.
GUID must be a 12-digit number (per the Aadhaar-number schema note);
phone, if given, must be 10 digits and is unique across guardians.
- **Student management** — list, view, add, update, delete students.
Roll number is up to 3 digits; each student must reference an
existing guardian's GUID (add the guardian first).
- **Attendance management** — mark attendance (P/A) for a student on a
date (marking the same student+date again updates it instead of
erroring), view attendance by date or by student, and delete records.
Deleting a guardian who still has students is blocked by the foreign
key (delete or reassign their students first). Deleting a student
cascades and removes their attendance records automatically.
## Notes
- Uses [PyMySQL](https://pymysql.readthedocs.io/), a pure-Python MySQL
client — no system libraries (like `libmysqlclient`) required.
- All input is validated against the schema's constraints (max lengths,
digit counts, date format, P/A status) before hitting the database,
and database-level errors (duplicate keys, foreign key violations)
are caught and shown as friendly messages rather than crashing.
- Tested end-to-end (connect → create DB/tables → full CRUD → constraint
error handling → cascade delete) against a local MySQL/MariaDB
instance before delivery.
+18
View File
@@ -0,0 +1,18 @@
"""
Configuration for the Student Management System.
Connection settings are read from environment variables first, falling back
to the defaults below (which match the assignment's requirements). This
means you can run the app as-is, or override any value like:
DB_HOST=10.0.1.40 DB_USER=root DB_PASSWORD=secret python3 main.py
"""
import os
DB_HOST = os.environ.get("DB_HOST", "10.0.1.40")
DB_PORT = int(os.environ.get("DB_PORT", "3306"))
DB_USER = os.environ.get("DB_USER", "root")
DB_PASSWORD = os.environ.get("DB_PASSWORD", "")
DB_NAME = os.environ.get("DB_NAME", "student_db")
DB_CONNECT_TIMEOUT = int(os.environ.get("DB_CONNECT_TIMEOUT", "10"))
+205
View File
@@ -0,0 +1,205 @@
"""
CRUD operations for GUARDIAN, STUDENT and ATTENDANCE.
Every function takes an open pymysql connection as its first argument and
manages its own commit/rollback. All functions return plain data
(dicts/lists/bool) so this module has no dependency on the UI layer.
"""
import pymysql
class CrudError(Exception):
"""Raised for expected, user-facing data errors (bad FK, duplicate key, etc)."""
pass
def _run(conn, sql, params=None, fetch=None):
"""
Helper: execute a statement, commit, and optionally fetch results.
fetch: None (no fetch), 'one', or 'all'
Translates common MySQL errors into CrudError with a friendly message.
"""
try:
with conn.cursor() as cur:
cur.execute(sql, params or ())
result = None
if fetch == "one":
result = cur.fetchone()
elif fetch == "all":
result = cur.fetchall()
conn.commit()
return result if fetch else cur.rowcount
except pymysql.err.IntegrityError as e:
conn.rollback()
code = e.args[0] if e.args else None
if code == 1062:
raise CrudError("A record with that primary/unique key already exists.")
if code in (1451, 1452):
raise CrudError(
"That operation violates a foreign key constraint "
"(referenced record missing, or still referenced by other rows)."
)
raise CrudError(f"Database integrity error: {e}")
except pymysql.err.DataError as e:
conn.rollback()
raise CrudError(f"Invalid data for one or more fields: {e}")
except pymysql.err.MySQLError as e:
conn.rollback()
raise CrudError(f"Database error: {e}")
# ---------------------------------------------------------------- GUARDIAN --
def create_guardian(conn, guid, gname, gphone, gaddress):
_run(
conn,
"INSERT INTO GUARDIAN (GUID, GName, GPhone, GAddress) VALUES (%s, %s, %s, %s)",
(guid, gname, gphone, gaddress),
)
def get_guardian(conn, guid):
return _run(conn, "SELECT * FROM GUARDIAN WHERE GUID = %s", (guid,), fetch="one")
def list_guardians(conn):
return _run(conn, "SELECT * FROM GUARDIAN ORDER BY GUID", fetch="all")
def update_guardian(conn, guid, gname=None, gphone=None, gaddress=None):
fields, params = [], []
if gname is not None:
fields.append("GName = %s")
params.append(gname)
if gphone is not None:
fields.append("GPhone = %s")
params.append(gphone)
if gaddress is not None:
fields.append("GAddress = %s")
params.append(gaddress)
if not fields:
return 0
params.append(guid)
return _run(conn, f"UPDATE GUARDIAN SET {', '.join(fields)} WHERE GUID = %s", params)
def delete_guardian(conn, guid):
return _run(conn, "DELETE FROM GUARDIAN WHERE GUID = %s", (guid,))
# ----------------------------------------------------------------- STUDENT --
def create_student(conn, roll_number, sname, sdateofbirth, guid):
_run(
conn,
"INSERT INTO STUDENT (RollNumber, SName, SDateofBirth, GUID) VALUES (%s, %s, %s, %s)",
(roll_number, sname, sdateofbirth, guid),
)
def get_student(conn, roll_number):
return _run(conn, "SELECT * FROM STUDENT WHERE RollNumber = %s", (roll_number,), fetch="one")
def list_students(conn):
return _run(
conn,
"""
SELECT s.RollNumber, s.SName, s.SDateofBirth, s.GUID, g.GName AS GuardianName
FROM STUDENT s
LEFT JOIN GUARDIAN g ON s.GUID = g.GUID
ORDER BY s.RollNumber
""",
fetch="all",
)
def update_student(conn, roll_number, sname=None, sdateofbirth=None, guid=None):
fields, params = [], []
if sname is not None:
fields.append("SName = %s")
params.append(sname)
if sdateofbirth is not None:
fields.append("SDateofBirth = %s")
params.append(sdateofbirth)
if guid is not None:
fields.append("GUID = %s")
params.append(guid)
if not fields:
return 0
params.append(roll_number)
return _run(conn, f"UPDATE STUDENT SET {', '.join(fields)} WHERE RollNumber = %s", params)
def delete_student(conn, roll_number):
return _run(conn, "DELETE FROM STUDENT WHERE RollNumber = %s", (roll_number,))
# --------------------------------------------------------------- ATTENDANCE --
def mark_attendance(conn, attendance_date, roll_number, status):
"""Insert attendance; if a record for that date+student exists, update it instead."""
existing = _run(
conn,
"SELECT 1 FROM ATTENDANCE WHERE AttendanceDate = %s AND RollNumber = %s",
(attendance_date, roll_number),
fetch="one",
)
if existing:
_run(
conn,
"UPDATE ATTENDANCE SET AttendanceStatus = %s "
"WHERE AttendanceDate = %s AND RollNumber = %s",
(status, attendance_date, roll_number),
)
return "updated"
else:
_run(
conn,
"INSERT INTO ATTENDANCE (AttendanceDate, RollNumber, AttendanceStatus) "
"VALUES (%s, %s, %s)",
(attendance_date, roll_number, status),
)
return "created"
def get_attendance(conn, attendance_date, roll_number):
return _run(
conn,
"SELECT * FROM ATTENDANCE WHERE AttendanceDate = %s AND RollNumber = %s",
(attendance_date, roll_number),
fetch="one",
)
def list_attendance_by_date(conn, attendance_date):
return _run(
conn,
"""
SELECT a.AttendanceDate, a.RollNumber, s.SName, a.AttendanceStatus
FROM ATTENDANCE a
JOIN STUDENT s ON a.RollNumber = s.RollNumber
WHERE a.AttendanceDate = %s
ORDER BY a.RollNumber
""",
(attendance_date,),
fetch="all",
)
def list_attendance_by_student(conn, roll_number):
return _run(
conn,
"SELECT * FROM ATTENDANCE WHERE RollNumber = %s ORDER BY AttendanceDate",
(roll_number,),
fetch="all",
)
def delete_attendance(conn, attendance_date, roll_number):
return _run(
conn,
"DELETE FROM ATTENDANCE WHERE AttendanceDate = %s AND RollNumber = %s",
(attendance_date, roll_number),
)
+135
View File
@@ -0,0 +1,135 @@
"""
Database layer: connect to MySQL, verify/create the student_db database,
and verify/create the STUDENT, GUARDIAN and ATTENDANCE tables per the
schema (Tables 9.3, 9.4, 9.5).
Schema notes:
- GUARDIAN.GUID is the primary key (12-digit Aadhaar number, stored as CHAR(12)).
- STUDENT.GUID is a foreign key referencing GUARDIAN.GUID.
- ATTENDANCE has a composite primary key (AttendanceDate, RollNumber), and
RollNumber is also a foreign key referencing STUDENT.RollNumber.
- Because STUDENT references GUARDIAN, GUARDIAN must be created first.
"""
import sys
import pymysql
import pymysql.cursors
import config
# Table creation statements, in dependency order (GUARDIAN -> STUDENT -> ATTENDANCE)
TABLE_DEFINITIONS = {
"GUARDIAN": """
CREATE TABLE GUARDIAN (
GUID CHAR(12) NOT NULL,
GName VARCHAR(20) NOT NULL,
GPhone CHAR(10) NULL,
GAddress VARCHAR(30) NOT NULL,
PRIMARY KEY (GUID),
UNIQUE (GPhone)
) ENGINE=InnoDB
""",
"STUDENT": """
CREATE TABLE STUDENT (
RollNumber INT NOT NULL,
SName VARCHAR(20) NOT NULL,
SDateofBirth DATE NOT NULL,
GUID CHAR(12) NOT NULL,
PRIMARY KEY (RollNumber),
FOREIGN KEY (GUID) REFERENCES GUARDIAN(GUID)
ON DELETE RESTRICT ON UPDATE CASCADE
) ENGINE=InnoDB
""",
"ATTENDANCE": """
CREATE TABLE ATTENDANCE (
AttendanceDate DATE NOT NULL,
RollNumber INT NOT NULL,
AttendanceStatus CHAR(1) NOT NULL,
PRIMARY KEY (AttendanceDate, RollNumber),
FOREIGN KEY (RollNumber) REFERENCES STUDENT(RollNumber)
ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT chk_attendance_status CHECK (AttendanceStatus IN ('P', 'A'))
) ENGINE=InnoDB
""",
}
# Creation order matters because of foreign keys.
TABLE_ORDER = ["GUARDIAN", "STUDENT", "ATTENDANCE"]
def connect_server(database=None):
"""Connect to the MySQL server (optionally selecting a database)."""
try:
conn = pymysql.connect(
host=config.DB_HOST,
port=config.DB_PORT,
user=config.DB_USER,
password=config.DB_PASSWORD,
database=database,
connect_timeout=config.DB_CONNECT_TIMEOUT,
cursorclass=pymysql.cursors.DictCursor,
autocommit=False,
)
return conn
except pymysql.err.OperationalError as e:
print(f"\n[ERROR] Could not connect to MySQL server at "
f"{config.DB_HOST}:{config.DB_PORT} as user '{config.DB_USER}'.")
print(f" Details: {e}")
sys.exit(1)
def ensure_database():
"""Verify the student_db database exists; create it if it does not."""
conn = connect_server(database=None)
try:
with conn.cursor() as cur:
cur.execute("SHOW DATABASES LIKE %s", (config.DB_NAME,))
exists = cur.fetchone() is not None
if exists:
print(f"[OK] Database '{config.DB_NAME}' found.")
else:
print(f"[..] Database '{config.DB_NAME}' not found. Creating it...")
cur.execute(
f"CREATE DATABASE `{config.DB_NAME}` "
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"
)
conn.commit()
print(f"[OK] Database '{config.DB_NAME}' created.")
finally:
conn.close()
def ensure_tables(conn):
"""Verify each required table exists; create any that are missing."""
with conn.cursor() as cur:
cur.execute("SHOW TABLES")
existing = {row[f"Tables_in_{config.DB_NAME}"] for row in cur.fetchall()}
for table in TABLE_ORDER:
if table in existing:
print(f"[OK] Table '{table}' found.")
else:
print(f"[..] Table '{table}' not found. Creating it...")
with conn.cursor() as cur:
cur.execute(TABLE_DEFINITIONS[table])
conn.commit()
print(f"[OK] Table '{table}' created.")
def get_connection():
"""
Full startup sequence:
1. Ensure the student_db database exists (creating it if needed).
2. Connect to student_db.
3. Ensure all required tables exist (creating any that are missing).
Returns an open connection to student_db, ready for use.
"""
print(f"Connecting to MySQL server at {config.DB_HOST}:{config.DB_PORT} ...")
ensure_database()
conn = connect_server(database=config.DB_NAME)
print(f"[OK] Connected to database '{config.DB_NAME}'.")
ensure_tables(conn)
return conn
+337
View File
@@ -0,0 +1,337 @@
#!/usr/bin/env python3
"""
Student Management System
==========================
Terminal CRUD application for STUDENT / GUARDIAN / ATTENDANCE, backed by
MySQL on 10.0.1.40:3306 (student_db).
On startup this:
1. Connects to the MySQL server.
2. Verifies (or creates) the student_db database.
3. Verifies (or creates) the STUDENT, GUARDIAN and ATTENDANCE tables.
4. Launches an interactive terminal menu for CRUD operations.
Run:
python3 main.py
"""
import sys
import pymysql
import crud
import db
from ui_helpers import (
clear_hint,
pause,
print_table,
prompt_attendance_status,
prompt_date,
prompt_guid,
prompt_int,
prompt_phone,
prompt_str,
)
# ============================================================ GUARDIAN UI ==
def menu_guardians(conn):
while True:
clear_hint()
print("GUARDIAN MANAGEMENT")
print(" 1. List all guardians")
print(" 2. View a guardian")
print(" 3. Add a guardian")
print(" 4. Update a guardian")
print(" 5. Delete a guardian")
print(" 0. Back to main menu")
choice = input("\nChoice: ").strip()
try:
if choice == "1":
print()
print_table(crud.list_guardians(conn))
pause()
elif choice == "2":
guid = prompt_guid()
row = crud.get_guardian(conn, guid)
print()
print_table([row] if row else [])
pause()
elif choice == "3":
add_guardian(conn)
elif choice == "4":
update_guardian(conn)
elif choice == "5":
delete_guardian(conn)
elif choice == "0":
return
else:
print(" Invalid choice.")
except crud.CrudError as e:
print(f"\n [ERROR] {e}")
pause()
def add_guardian(conn):
print("\n-- Add Guardian --")
guid = prompt_guid()
gname = prompt_str("Guardian name", max_len=20)
gphone = prompt_phone()
gaddress = prompt_str("Guardian address", max_len=30)
crud.create_guardian(conn, guid, gname, gphone, gaddress)
print(f" [OK] Guardian {guid} added.")
pause()
def update_guardian(conn):
print("\n-- Update Guardian --")
guid = prompt_guid()
existing = crud.get_guardian(conn, guid)
if not existing:
print(" No guardian found with that GUID.")
pause()
return
print(f" Current: {existing}")
print(" (Press Enter to leave a field unchanged)")
gname = prompt_str("New name", max_len=20, allow_blank_skip=True)
gphone_raw = input("New phone (10 digits, blank = unchanged, 'clear' = remove): ").strip()
gphone = existing["GPhone"]
if gphone_raw.lower() == "clear":
gphone = None
elif gphone_raw:
gphone = prompt_phone_validate(gphone_raw)
if gphone is None:
print(" Invalid phone — update aborted for that field.")
gphone = existing["GPhone"]
gaddress = prompt_str("New address", max_len=30, allow_blank_skip=True)
crud.update_guardian(conn, guid, gname=gname, gphone=gphone, gaddress=gaddress)
print(" [OK] Guardian updated.")
pause()
def prompt_phone_validate(raw):
import re
return raw if re.match(r"^\d{10}$", raw) else None
def delete_guardian(conn):
print("\n-- Delete Guardian --")
guid = prompt_guid()
confirm = input(f" Type 'yes' to confirm deleting guardian {guid}: ").strip().lower()
if confirm != "yes":
print(" Cancelled.")
pause()
return
rows = crud.delete_guardian(conn, guid)
if rows:
print(" [OK] Guardian deleted.")
else:
print(" No guardian found with that GUID.")
pause()
# ============================================================= STUDENT UI ==
def menu_students(conn):
while True:
clear_hint()
print("STUDENT MANAGEMENT")
print(" 1. List all students")
print(" 2. View a student")
print(" 3. Add a student")
print(" 4. Update a student")
print(" 5. Delete a student")
print(" 0. Back to main menu")
choice = input("\nChoice: ").strip()
try:
if choice == "1":
print()
print_table(crud.list_students(conn))
pause()
elif choice == "2":
roll = prompt_int("Roll number", max_digits=3)
row = crud.get_student(conn, roll)
print()
print_table([row] if row else [])
pause()
elif choice == "3":
add_student(conn)
elif choice == "4":
update_student(conn)
elif choice == "5":
delete_student(conn)
elif choice == "0":
return
else:
print(" Invalid choice.")
except crud.CrudError as e:
print(f"\n [ERROR] {e}")
pause()
def add_student(conn):
print("\n-- Add Student --")
roll = prompt_int("Roll number (max 3 digits)", max_digits=3)
sname = prompt_str("Student name", max_len=20)
dob = prompt_date("Date of birth")
print(" (Guardian must already exist — add them first via Guardian Management if needed)")
guid = prompt_guid()
crud.create_student(conn, roll, sname, dob, guid)
print(f" [OK] Student {roll} added.")
pause()
def update_student(conn):
print("\n-- Update Student --")
roll = prompt_int("Roll number", max_digits=3)
existing = crud.get_student(conn, roll)
if not existing:
print(" No student found with that roll number.")
pause()
return
print(f" Current: {existing}")
print(" (Press Enter to leave a field unchanged)")
sname = prompt_str("New name", max_len=20, allow_blank_skip=True)
dob = prompt_date("New date of birth", allow_blank_skip=True)
guid = prompt_guid("New guardian GUID", allow_blank_skip=True)
crud.update_student(conn, roll, sname=sname, sdateofbirth=dob, guid=guid)
print(" [OK] Student updated.")
pause()
def delete_student(conn):
print("\n-- Delete Student --")
roll = prompt_int("Roll number", max_digits=3)
confirm = input(f" Type 'yes' to confirm deleting student {roll} "
f"(also deletes their attendance records): ").strip().lower()
if confirm != "yes":
print(" Cancelled.")
pause()
return
rows = crud.delete_student(conn, roll)
if rows:
print(" [OK] Student deleted.")
else:
print(" No student found with that roll number.")
pause()
# =========================================================== ATTENDANCE UI ==
def menu_attendance(conn):
while True:
clear_hint()
print("ATTENDANCE MANAGEMENT")
print(" 1. Mark / update attendance for a student")
print(" 2. View attendance by date")
print(" 3. View attendance history for a student")
print(" 4. Delete an attendance record")
print(" 0. Back to main menu")
choice = input("\nChoice: ").strip()
try:
if choice == "1":
mark_attendance(conn)
elif choice == "2":
d = prompt_date("Date")
print()
print_table(crud.list_attendance_by_date(conn, d))
pause()
elif choice == "3":
roll = prompt_int("Roll number", max_digits=3)
print()
print_table(crud.list_attendance_by_student(conn, roll))
pause()
elif choice == "4":
delete_attendance(conn)
elif choice == "0":
return
else:
print(" Invalid choice.")
except crud.CrudError as e:
print(f"\n [ERROR] {e}")
pause()
def mark_attendance(conn):
print("\n-- Mark Attendance --")
roll = prompt_int("Roll number", max_digits=3)
if not crud.get_student(conn, roll):
print(" No student found with that roll number.")
pause()
return
d = prompt_date("Date")
status = prompt_attendance_status()
result = crud.mark_attendance(conn, d, roll, status)
print(f" [OK] Attendance {result} for student {roll} on {d}: {status}")
pause()
def delete_attendance(conn):
print("\n-- Delete Attendance Record --")
roll = prompt_int("Roll number", max_digits=3)
d = prompt_date("Date")
confirm = input(" Type 'yes' to confirm: ").strip().lower()
if confirm != "yes":
print(" Cancelled.")
pause()
return
rows = crud.delete_attendance(conn, d, roll)
if rows:
print(" [OK] Attendance record deleted.")
else:
print(" No matching attendance record found.")
pause()
# ================================================================ MAIN ====
def main_menu(conn):
while True:
clear_hint()
print("STUDENT MANAGEMENT SYSTEM")
print(" 1. Guardian management")
print(" 2. Student management")
print(" 3. Attendance management")
print(" 0. Exit")
choice = input("\nChoice: ").strip()
if choice == "1":
menu_guardians(conn)
elif choice == "2":
menu_students(conn)
elif choice == "3":
menu_attendance(conn)
elif choice == "0":
print("\nGoodbye!")
return
else:
print(" Invalid choice.")
def main():
print("=" * 60)
print(" STUDENT MANAGEMENT SYSTEM - Startup")
print("=" * 60)
conn = db.get_connection()
print("\nAll checks passed. Starting application...\n")
try:
main_menu(conn)
except KeyboardInterrupt:
print("\n\nInterrupted. Exiting.")
except pymysql.err.MySQLError as e:
print(f"\n[ERROR] Lost database connection: {e}")
sys.exit(1)
finally:
conn.close()
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
PyMySQL>=1.1.0
+118
View File
@@ -0,0 +1,118 @@
"""Small input-prompting and formatting helpers shared by the menu screens."""
import datetime
import re
GUID_RE = re.compile(r"^\d{12}$")
PHONE_RE = re.compile(r"^\d{10}$")
def pause():
input("\nPress Enter to continue...")
def clear_hint():
print("\n" + "-" * 60 + "\n")
def prompt_str(label, max_len=None, required=True, allow_blank_skip=False):
"""Prompt for a string. allow_blank_skip=True returns None on empty input (for 'leave unchanged')."""
while True:
suffix = f" (max {max_len} chars)" if max_len else ""
val = input(f"{label}{suffix}: ").strip()
if not val:
if allow_blank_skip:
return None
if required:
print(" This field is required.")
continue
return ""
if max_len and len(val) > max_len:
print(f" Too long — max {max_len} characters.")
continue
return val
def prompt_int(label, max_digits=None, allow_blank_skip=False):
while True:
val = input(f"{label}: ").strip()
if not val:
if allow_blank_skip:
return None
print(" This field is required.")
continue
if not val.isdigit():
print(" Please enter a whole number.")
continue
if max_digits and len(val) > max_digits:
print(f" Too many digits — max {max_digits}.")
continue
return int(val)
def prompt_guid(label="Guardian ID / GUID (12-digit Aadhaar number)", allow_blank_skip=False):
while True:
val = input(f"{label}: ").strip()
if not val:
if allow_blank_skip:
return None
print(" This field is required.")
continue
if not GUID_RE.match(val):
print(" GUID must be exactly 12 digits.")
continue
return val
def prompt_phone(label="Guardian phone (10 digits, optional)", allow_blank_skip=True):
while True:
val = input(f"{label}: ").strip()
if not val:
return None # NULL is allowed for GPhone
if not PHONE_RE.match(val):
print(" Phone must be exactly 10 digits (or leave blank).")
continue
return val
def prompt_date(label, allow_blank_skip=False):
while True:
val = input(f"{label} (YYYY-MM-DD): ").strip()
if not val:
if allow_blank_skip:
return None
print(" This field is required.")
continue
try:
return datetime.datetime.strptime(val, "%Y-%m-%d").date()
except ValueError:
print(" Invalid date — use format YYYY-MM-DD, e.g. 2015-08-21.")
def prompt_attendance_status(label="Status (P=present, A=absent)"):
while True:
val = input(f"{label}: ").strip().upper()
if val in ("P", "A"):
return val
print(" Please enter 'P' or 'A'.")
def print_table(rows, columns=None):
"""Pretty-print a list of dict rows as a simple aligned table."""
if not rows:
print(" (no records found)")
return
if columns is None:
columns = list(rows[0].keys())
widths = {}
for col in columns:
widths[col] = max(len(str(col)), *(len(str(r.get(col, ""))) for r in rows))
header = " " + " | ".join(str(col).ljust(widths[col]) for col in columns)
print(header)
print(" " + "-+-".join("-" * widths[col] for col in columns))
for r in rows:
print(" " + " | ".join(str(r.get(col, "")).ljust(widths[col]) for col in columns))
print(f"\n ({len(rows)} record{'s' if len(rows) != 1 else ''})")