Medical-Validator / app /database.py
saifisvibin's picture
Fix database path for HF Spaces (use /tmp for writable storage)
306fa3b
"""
SQLite database layer for Projects feature.
Manages projects and validation history.
"""
import sqlite3
import json
import logging
import os
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional, Any
logger = logging.getLogger(__name__)
def get_database_path():
"""Get writable database path based on environment."""
# Check if running on Hugging Face Spaces
if os.environ.get("SPACE_ID"):
# Use /tmp for HF Spaces (writable directory)
db_dir = "/tmp"
else:
# Local development - use current directory
db_dir = "."
return os.path.join(db_dir, "projects.db")
class ProjectDatabase:
"""Database manager for projects and validations."""
def __init__(self, db_path: str = None):
"""Initialize database connection and create tables if needed."""
self.db_path = db_path or get_database_path()
self._init_database()
def _init_database(self):
"""Create tables if they don't exist."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
# Projects table
cursor.execute("""
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Validations table
cursor.execute("""
CREATE TABLE IF NOT EXISTS validations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER,
validation_type TEXT NOT NULL,
template_key TEXT,
filename TEXT,
status TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
results_json TEXT,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
)
""")
conn.commit()
logger.info(f"Database initialized at {self.db_path}")
# Project CRUD operations
def create_project(self, name: str, description: str = "") -> int:
"""Create a new project."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
try:
cursor.execute(
"INSERT INTO projects (name, description) VALUES (?, ?)",
(name, description)
)
conn.commit()
project_id = cursor.lastrowid
logger.info(f"Created project: {name} (ID: {project_id})")
return project_id
except sqlite3.IntegrityError:
raise ValueError(f"Project '{name}' already exists")
def list_projects(self) -> List[Dict[str, Any]]:
"""List all projects."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT p.*, COUNT(v.id) as validation_count
FROM projects p
LEFT JOIN validations v ON p.id = v.project_id
GROUP BY p.id
ORDER BY p.created_at DESC
""")
projects = [dict(row) for row in cursor.fetchall()]
return projects
def get_project(self, project_id: int) -> Optional[Dict[str, Any]]:
"""Get a specific project by ID."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT p.*, COUNT(v.id) as validation_count
FROM projects p
LEFT JOIN validations v ON p.id = v.project_id
WHERE p.id = ?
GROUP BY p.id
""", (project_id,))
row = cursor.fetchone()
return dict(row) if row else None
def delete_project(self, project_id: int) -> bool:
"""Delete a project and all associated validations."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM projects WHERE id = ?", (project_id,))
conn.commit()
deleted = cursor.rowcount > 0
if deleted:
logger.info(f"Deleted project ID: {project_id}")
return deleted
# Validation operations
def save_validation(
self,
project_id: Optional[int],
validation_type: str,
filename: str,
status: str,
results: Dict[str, Any],
template_key: Optional[str] = None
) -> int:
"""Save a validation result to a project."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("""
INSERT INTO validations
(project_id, validation_type, template_key, filename, status, results_json)
VALUES (?, ?, ?, ?, ?, ?)
""", (
project_id,
validation_type,
template_key,
filename,
status,
json.dumps(results)
))
conn.commit()
validation_id = cursor.lastrowid
logger.info(f"Saved {validation_type} validation (ID: {validation_id}) to project {project_id}")
return validation_id
def get_project_validations(self, project_id: int) -> List[Dict[str, Any]]:
"""Get all validations for a specific project."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM validations
WHERE project_id = ?
ORDER BY created_at DESC
""", (project_id,))
validations = []
for row in cursor.fetchall():
validation = dict(row)
# Parse JSON results
if validation['results_json']:
validation['results'] = json.loads(validation['results_json'])
del validation['results_json']
validations.append(validation)
return validations
def get_recent_validations(self, limit: int = 50) -> List[Dict[str, Any]]:
"""Get recent validations across all projects."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("""
SELECT v.*, p.name as project_name
FROM validations v
LEFT JOIN projects p ON v.project_id = p.id
ORDER BY v.created_at DESC
LIMIT ?
""", (limit,))
validations = []
for row in cursor.fetchall():
validation = dict(row)
if validation['results_json']:
validation['results'] = json.loads(validation['results_json'])
del validation['results_json']
validations.append(validation)
return validations
# Global database instance
db = ProjectDatabase()