omnidiag / scripts /seed_db.py
yahyoha's picture
docs: add architecture plans and utility scripts
01f3f99
Raw
History Blame Contribute Delete
16.7 kB
#!/usr/bin/env python3
"""
OmniDiag β€” Database Seed Script
=================================
Idempotent script to populate the database with demo data.
Safe to run multiple times β€” uses ON CONFLICT DO NOTHING / get-or-create
pattern for all seed data.
Usage:
# Default: uses DATABASE_URL from environment (or SQLite fallback)
python scripts/seed_db.py
# Explicit PostgreSQL connection:
DATABASE_URL=postgresql+asyncpg://omnidiag:omnidiag_pass@localhost:5432/omnidiag_db \\
python scripts/seed_db.py
Environment Variables:
DATABASE_URL (optional, default: sqlite+aiosqlite:///./omnidiag_dev.db)
"""
import asyncio
import os
import sys
import uuid
from datetime import date, datetime, timezone
# Ensure project root is on sys.path so we can import backend modules
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import bcrypt
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from backend.database import Base, DATABASE_URL
from backend.db_models import (
Role,
User,
Patient,
Prediction,
)
# ── Password Hashing ────────────────────────────────────────────────────────
def hash_password(password: str) -> str:
"""Hash a password using bcrypt."""
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
# ── Helpers ─────────────────────────────────────────────────────────────────
def utcnow() -> datetime:
"""Return current UTC datetime."""
return datetime.now(timezone.utc)
# ── Seed Data ───────────────────────────────────────────────────────────────
ROLES = [
{"name": "super_admin", "description": "Full system access β€” user management, audit review, model administration"},
{"name": "doctor", "description": "Clinical access β€” predict, explain, counterfactuals, patient records"},
{"name": "nurse", "description": "Limited clinical access β€” predict, view patient records"},
{"name": "viewer", "description": "Read-only access β€” view predictions and patient data"},
]
USERS = [
{
"email": "admin@omnidiag.com",
"password": "Admin@123",
"full_name": "Admin User",
"role": "super_admin",
},
{
"email": "doctor@omnidiag.com",
"password": "Doctor@123",
"full_name": "Dr. Sarah Al-Khalid",
"role": "doctor",
},
]
# Heart disease patients (from frontend/src/mockPatients.js heart_disease entries)
CAD_PATIENTS = [
{
"mrn": "CAD-001",
"full_name": "Ahmed Al-Rashid",
"date_of_birth": date(1970, 5, 15),
"gender": "M",
"contact_email": "ahmed.alrashid@example.com",
"prediction": {
"disease": "heart_disease",
"input_features": {
"Age": 54,
"Sex": "M",
"ChestPainType": "ATA",
"RestingBP": 140,
"Cholesterol": 289,
"FastingBS": 0,
"RestingECG": "Normal",
"MaxHR": 122,
"ExerciseAngina": "N",
"Oldpeak": 0.0,
"ST_Slope": "Flat",
},
"prediction": 1,
"confidence": 0.72,
"diagnosis": "Positive",
},
},
{
"mrn": "CAD-002",
"full_name": "Fatima Hassan",
"date_of_birth": date(1962, 8, 22),
"gender": "F",
"contact_email": "fatima.hassan@example.com",
"prediction": {
"disease": "heart_disease",
"input_features": {
"Age": 62,
"Sex": "F",
"ChestPainType": "ASY",
"RestingBP": 158,
"Cholesterol": 340,
"FastingBS": 1,
"RestingECG": "LVH",
"MaxHR": 98,
"ExerciseAngina": "Y",
"Oldpeak": 2.3,
"ST_Slope": "Down",
},
"prediction": 1,
"confidence": 0.91,
"diagnosis": "Positive",
},
},
{
"mrn": "CAD-003",
"full_name": "Khalid Othman",
"date_of_birth": date(1979, 11, 3),
"gender": "M",
"contact_email": "khalid.othman@example.com",
"prediction": {
"disease": "heart_disease",
"input_features": {
"Age": 45,
"Sex": "M",
"ChestPainType": "NAP",
"RestingBP": 120,
"Cholesterol": 210,
"FastingBS": 0,
"RestingECG": "Normal",
"MaxHR": 160,
"ExerciseAngina": "N",
"Oldpeak": 0.5,
"ST_Slope": "Up",
},
"prediction": 0,
"confidence": 0.84,
"diagnosis": "Negative",
},
},
]
# Diabetes patients (from frontend/src/mockPatients.js diabetes entries)
DM_PATIENTS = [
{
"mrn": "DM-001",
"full_name": "Layla Mansour",
"date_of_birth": date(1966, 3, 10),
"gender": "F",
"contact_email": "layla.mansour@example.com",
"prediction": {
"disease": "diabetes",
"input_features": {
"HighBP": 1,
"HighChol": 1,
"CholCheck": 1,
"BMI": 32.4,
"Smoker": 0,
"Stroke": 0,
"HeartDiseaseorAttack": 0,
"PhysActivity": 0,
"Fruits": 0,
"Veggies": 0,
"HvyAlcoholConsump": 0,
"AnyHealthcare": 1,
"NoDocbcCost": 0,
"GenHlth": 3,
"MentHlth": 12,
"PhysHlth": 18,
"DiffWalk": 1,
"Sex": 0,
"Age": 10,
"Education": 3,
"Income": 4,
},
"prediction": 1,
"confidence": 0.87,
"diagnosis": "Positive",
},
},
{
"mrn": "DM-002",
"full_name": "Mohammed Al-Sayed",
"date_of_birth": date(1960, 7, 28),
"gender": "M",
"contact_email": "mohammed.alsayed@example.com",
"prediction": {
"disease": "diabetes",
"input_features": {
"HighBP": 1,
"HighChol": 1,
"CholCheck": 1,
"BMI": 28.7,
"Smoker": 1,
"Stroke": 0,
"HeartDiseaseorAttack": 1,
"PhysActivity": 0,
"Fruits": 1,
"Veggies": 0,
"HvyAlcoholConsump": 0,
"AnyHealthcare": 1,
"NoDocbcCost": 0,
"GenHlth": 4,
"MentHlth": 8,
"PhysHlth": 22,
"DiffWalk": 1,
"Sex": 1,
"Age": 11,
"Education": 2,
"Income": 3,
},
"prediction": 1,
"confidence": 0.93,
"diagnosis": "Positive",
},
},
]
# ── Main Seeder ─────────────────────────────────────────────────────────────
async def seed_database(db_url: str) -> None:
"""
Seed the database with initial demo data.
This function is idempotent β€” safe to run multiple times.
Uses ON CONFLICT DO NOTHING / get-or-create patterns throughout.
"""
# Create engine and session
engine = create_async_engine(db_url, echo=False)
session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
# Stats tracker
stats = {"roles": 0, "users": 0, "patients": 0, "predictions": 0}
async with session_factory() as session:
async with session.begin():
# ── 1. Create tables if they don't exist ───────────────────────
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
# ── 2. Seed roles ─────────────────────────────────────────────
for role_data in ROLES:
# Check if role exists
result = await session.execute(
text("SELECT id FROM roles WHERE name = :name"),
{"name": role_data["name"]},
)
existing = result.scalar_one_or_none()
if existing is None:
role = Role(name=role_data["name"], description=role_data["description"])
session.add(role)
stats["roles"] += 1
print(f" βž• Created role: {role_data['name']}")
else:
print(f" βœ“ Role already exists: {role_data['name']}")
await session.flush() # Ensure roles have IDs
# ── 3. Seed users ─────────────────────────────────────────────
for user_data in USERS:
result = await session.execute(
text("SELECT id FROM users WHERE email = :email"),
{"email": user_data["email"]},
)
existing = result.scalar_one_or_none()
if existing is None:
user = User(
id=str(uuid.uuid4()),
email=user_data["email"],
hashed_password=hash_password(user_data["password"]),
full_name=user_data["full_name"],
is_active=True,
)
session.add(user)
await session.flush() # Get user.id
# Assign role
role_result = await session.execute(
text("SELECT id FROM roles WHERE name = :name"),
{"name": user_data["role"]},
)
role_id = role_result.scalar_one()
await session.execute(
text(
"INSERT INTO user_roles (user_id, role_id, assigned_at) "
"VALUES (:user_id, :role_id, :assigned_at)"
),
{
"user_id": user.id,
"role_id": role_id,
"assigned_at": utcnow(),
},
)
stats["users"] += 1
print(f" βž• Created user: {user_data['email']} (role: {user_data['role']})")
else:
print(f" βœ“ User already exists: {user_data['email']}")
# Get doctor user ID for created_by fields
doctor_result = await session.execute(
text("SELECT id FROM users WHERE email = 'doctor@omnidiag.com'"),
)
doctor_id = doctor_result.scalar_one()
# ── 4. Seed CAD patients ──────────────────────────────────────
for pat_data in CAD_PATIENTS:
result = await session.execute(
text("SELECT id FROM patients WHERE mrn = :mrn"),
{"mrn": pat_data["mrn"]},
)
existing = result.scalar_one_or_none()
if existing is None:
patient_id = str(uuid.uuid4())
patient = Patient(
id=patient_id,
mrn=pat_data["mrn"],
full_name=pat_data["full_name"],
date_of_birth=pat_data["date_of_birth"],
gender=pat_data["gender"],
contact_email=pat_data["contact_email"],
created_by=doctor_id,
)
session.add(patient)
await session.flush()
# Create prediction
pred = pat_data["prediction"]
prediction = Prediction(
id=str(uuid.uuid4()),
patient_id=patient_id,
disease=pred["disease"],
input_features=pred["input_features"],
prediction=pred["prediction"],
confidence=pred["confidence"],
diagnosis=pred["diagnosis"],
created_by=doctor_id,
)
session.add(prediction)
stats["patients"] += 1
stats["predictions"] += 1
print(f" βž• Created CAD patient: {pat_data['full_name']} ({pat_data['mrn']})")
else:
print(f" βœ“ CAD patient already exists: {pat_data['full_name']}")
# ── 5. Seed Diabetes patients ─────────────────────────────────
for pat_data in DM_PATIENTS:
result = await session.execute(
text("SELECT id FROM patients WHERE mrn = :mrn"),
{"mrn": pat_data["mrn"]},
)
existing = result.scalar_one_or_none()
if existing is None:
patient_id = str(uuid.uuid4())
patient = Patient(
id=patient_id,
mrn=pat_data["mrn"],
full_name=pat_data["full_name"],
date_of_birth=pat_data["date_of_birth"],
gender=pat_data["gender"],
contact_email=pat_data["contact_email"],
created_by=doctor_id,
)
session.add(patient)
await session.flush()
# Create prediction
pred = pat_data["prediction"]
prediction = Prediction(
id=str(uuid.uuid4()),
patient_id=patient_id,
disease=pred["disease"],
input_features=pred["input_features"],
prediction=pred["prediction"],
confidence=pred["confidence"],
diagnosis=pred["diagnosis"],
created_by=doctor_id,
)
session.add(prediction)
stats["patients"] += 1
stats["predictions"] += 1
print(f" βž• Created DM patient: {pat_data['full_name']} ({pat_data['mrn']})")
else:
print(f" βœ“ DM patient already exists: {pat_data['full_name']}")
# ── Commit is handled by `async with session.begin()` ─────────────
# ── Print summary ──────────────────────────────────────────────────────
print()
print("=" * 50)
print("βœ… Database seeding complete!")
print("=" * 50)
print(f" βœ… Seeded {stats['roles']} roles")
print(f" βœ… Seeded {stats['users']} users")
print(f" βœ… Seeded {stats['patients']} patients")
print(f" βœ… Seeded {stats['predictions']} predictions")
print("=" * 50)
print()
print("Demo credentials:")
print(" Admin: admin@omnidiag.com / Admin@123")
print(" Doctor: doctor@omnidiag.com / Doctor@123")
print()
await engine.dispose()
def main() -> None:
"""Entry point β€” read DATABASE_URL from environment and run the seeder."""
db_url = os.getenv("DATABASE_URL", DATABASE_URL)
print(f"🌱 OmniDiag Database Seeder")
print(f" Database URL: {db_url}")
print()
asyncio.run(seed_database(db_url))
if __name__ == "__main__":
main()