| |
| """ |
| 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 |
|
|
| |
| 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, |
| ) |
|
|
| |
| def hash_password(password: str) -> str: |
| """Hash a password using bcrypt.""" |
| return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8") |
|
|
|
|
| |
| def utcnow() -> datetime: |
| """Return current UTC datetime.""" |
| return datetime.now(timezone.utc) |
|
|
|
|
| |
| 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", |
| }, |
| ] |
|
|
| |
| 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", |
| }, |
| }, |
| ] |
|
|
| |
| 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", |
| }, |
| }, |
| ] |
|
|
|
|
| |
| 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. |
| """ |
| |
| engine = create_async_engine(db_url, echo=False) |
| session_factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) |
|
|
| |
| stats = {"roles": 0, "users": 0, "patients": 0, "predictions": 0} |
|
|
| async with session_factory() as session: |
| async with session.begin(): |
| |
| async with engine.begin() as conn: |
| await conn.run_sync(Base.metadata.create_all) |
|
|
| |
| for role_data in ROLES: |
| |
| 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() |
|
|
| |
| 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() |
|
|
| |
| 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']}") |
|
|
| |
| doctor_result = await session.execute( |
| text("SELECT id FROM users WHERE email = 'doctor@omnidiag.com'"), |
| ) |
| doctor_id = doctor_result.scalar_one() |
|
|
| |
| 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() |
|
|
| |
| 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']}") |
|
|
| |
| 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() |
|
|
| |
| 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']}") |
|
|
| |
|
|
| |
| 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() |
|
|