| """ |
| Database Models and Connection Management |
| SQLite database for users, predictions, and feedback |
| """ |
|
|
| import os |
| import sqlite3 |
| from datetime import datetime, timedelta |
| from typing import Dict, List, Optional, Tuple |
| from contextlib import contextmanager |
| import json |
|
|
| class Database: |
| """Database management class for SQLite""" |
| |
| def __init__(self, db_path: str = '../data/app.db'): |
| self.db_path = os.path.abspath(db_path) |
| db_dir = os.path.dirname(self.db_path) |
| os.makedirs(db_dir, exist_ok=True) |
| self._init_database() |
| |
| @contextmanager |
| def get_connection(self): |
| """Get database connection with automatic commit/rollback""" |
| conn = sqlite3.connect(self.db_path) |
| conn.row_factory = sqlite3.Row |
| try: |
| yield conn |
| conn.commit() |
| except Exception as e: |
| conn.rollback() |
| raise e |
| finally: |
| conn.close() |
| |
| def _init_database(self): |
| """Initialize database tables""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS users ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id TEXT UNIQUE NOT NULL, |
| email TEXT, |
| phone TEXT, |
| baby_name TEXT NOT NULL, |
| baby_gender TEXT, |
| baby_birthday TEXT, |
| platform TEXT DEFAULT 'web', |
| language TEXT DEFAULT 'ar', |
| registration_date TEXT NOT NULL, |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP, |
| updated_at TEXT DEFAULT CURRENT_TIMESTAMP |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS predictions ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| user_id TEXT, |
| prediction TEXT NOT NULL, |
| confidence REAL NOT NULL, |
| audio_id TEXT UNIQUE NOT NULL, |
| model_type TEXT, |
| timestamp TEXT NOT NULL, |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (user_id) REFERENCES users(user_id) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS feedback ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| audio_id TEXT NOT NULL, |
| user_id TEXT, |
| predicted_label TEXT NOT NULL, |
| correct_label TEXT NOT NULL, |
| is_correct INTEGER NOT NULL, |
| confidence REAL, |
| category TEXT, |
| timestamp TEXT NOT NULL, |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP, |
| FOREIGN KEY (user_id) REFERENCES users(user_id) |
| ) |
| ''') |
| |
| |
| cursor.execute(''' |
| CREATE TABLE IF NOT EXISTS analytics_events ( |
| id INTEGER PRIMARY KEY AUTOINCREMENT, |
| event_name TEXT NOT NULL, |
| user_id TEXT, |
| event_data TEXT, |
| timestamp TEXT NOT NULL, |
| created_at TEXT DEFAULT CURRENT_TIMESTAMP |
| ) |
| ''') |
| |
| |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_user_id ON users(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_predictions_user_id ON predictions(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_predictions_timestamp ON predictions(timestamp)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_feedback_user_id ON feedback(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_feedback_audio_id ON feedback(audio_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_feedback_timestamp ON feedback(timestamp)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_analytics_user_id ON analytics_events(user_id)') |
| cursor.execute('CREATE INDEX IF NOT EXISTS idx_analytics_timestamp ON analytics_events(timestamp)') |
| |
| conn.commit() |
| |
| |
| |
| def create_user(self, user_data: Dict) -> str: |
| """Create a new user""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| |
| registration_date = user_data.get('registration_date', datetime.now().isoformat()) |
| |
| cursor.execute(''' |
| INSERT INTO users ( |
| user_id, email, phone, baby_name, baby_gender, baby_birthday, |
| platform, language, registration_date |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) |
| ''', ( |
| user_data.get('user_id'), |
| user_data.get('email'), |
| user_data.get('phone'), |
| user_data.get('baby_name'), |
| user_data.get('baby_gender'), |
| user_data.get('baby_birthday'), |
| user_data.get('platform', 'web'), |
| user_data.get('language', 'ar'), |
| registration_date |
| )) |
| |
| return user_data.get('user_id') |
| |
| def get_user(self, user_id: str) -> Optional[Dict]: |
| """Get user by user_id""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute('SELECT * FROM users WHERE user_id = ?', (user_id,)) |
| row = cursor.fetchone() |
| return dict(row) if row else None |
| |
| def get_all_users(self, limit: Optional[int] = None, offset: int = 0) -> List[Dict]: |
| """Get all users with pagination""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| if limit: |
| cursor.execute('SELECT * FROM users ORDER BY registration_date DESC LIMIT ? OFFSET ?', (limit, offset)) |
| else: |
| cursor.execute('SELECT * FROM users ORDER BY registration_date DESC') |
| return [dict(row) for row in cursor.fetchall()] |
| |
| def update_user(self, user_id: str, user_data: Dict) -> bool: |
| """Update user information""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| |
| updates = [] |
| values = [] |
| |
| for key in ['email', 'phone', 'baby_name', 'baby_gender', 'baby_birthday', 'platform', 'language']: |
| if key in user_data: |
| updates.append(f'{key} = ?') |
| values.append(user_data[key]) |
| |
| if not updates: |
| return False |
| |
| updates.append('updated_at = ?') |
| values.append(datetime.now().isoformat()) |
| values.append(user_id) |
| |
| cursor.execute(f''' |
| UPDATE users SET {", ".join(updates)} |
| WHERE user_id = ? |
| ''', values) |
| |
| return cursor.rowcount > 0 |
| |
| def delete_user(self, user_id: str) -> bool: |
| """Delete user""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute('DELETE FROM users WHERE user_id = ?', (user_id,)) |
| return cursor.rowcount > 0 |
| |
| def count_users(self) -> int: |
| """Count total users""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute('SELECT COUNT(*) as count FROM users') |
| return cursor.fetchone()['count'] |
| |
| |
| |
| def create_prediction(self, prediction_data: Dict): |
| """Save a prediction (recording)""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute(''' |
| INSERT INTO predictions ( |
| user_id, prediction, confidence, audio_id, model_type, timestamp |
| ) VALUES (?, ?, ?, ?, ?, ?) |
| ''', ( |
| prediction_data.get('user_id'), |
| prediction_data.get('prediction'), |
| prediction_data.get('confidence', 0), |
| prediction_data.get('audio_id'), |
| prediction_data.get('model_type'), |
| prediction_data.get('timestamp', datetime.now().isoformat()) |
| )) |
| |
| def count_predictions(self, user_id: Optional[str] = None) -> int: |
| """Count total predictions/recordings""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| if user_id: |
| cursor.execute('SELECT COUNT(*) as count FROM predictions WHERE user_id = ?', (user_id,)) |
| else: |
| cursor.execute('SELECT COUNT(*) as count FROM predictions') |
| return cursor.fetchone()['count'] |
| |
| |
| |
| def create_feedback(self, feedback_data: Dict): |
| """Save feedback""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute(''' |
| INSERT INTO feedback ( |
| audio_id, user_id, predicted_label, correct_label, |
| is_correct, confidence, category, timestamp |
| ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) |
| ''', ( |
| feedback_data.get('audio_id'), |
| feedback_data.get('user_id'), |
| feedback_data.get('predicted_label'), |
| feedback_data.get('correct_label'), |
| 1 if feedback_data.get('is_correct', False) else 0, |
| feedback_data.get('confidence'), |
| feedback_data.get('correct_label'), |
| feedback_data.get('timestamp', datetime.now().isoformat()) |
| )) |
| |
| def count_feedback(self, is_correct: Optional[bool] = None) -> int: |
| """Count feedback (optionally filtered by is_correct)""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| if is_correct is not None: |
| cursor.execute( |
| 'SELECT COUNT(*) as count FROM feedback WHERE is_correct = ?', |
| (1 if is_correct else 0,) |
| ) |
| else: |
| cursor.execute('SELECT COUNT(*) as count FROM feedback') |
| return cursor.fetchone()['count'] |
| |
| |
| |
| def get_user_stats(self) -> Dict: |
| """Get comprehensive user statistics""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| |
| |
| cursor.execute('SELECT COUNT(*) as count FROM users') |
| total_users = cursor.fetchone()['count'] |
| |
| |
| seven_days_ago = (datetime.now() - timedelta(days=7)).isoformat() |
| thirty_days_ago = (datetime.now() - timedelta(days=30)).isoformat() |
| |
| cursor.execute(''' |
| SELECT COUNT(DISTINCT user_id) as count FROM predictions |
| WHERE timestamp >= ? AND user_id IS NOT NULL |
| ''', (seven_days_ago,)) |
| active_7d = cursor.fetchone()['count'] |
| |
| cursor.execute(''' |
| SELECT COUNT(DISTINCT user_id) as count FROM predictions |
| WHERE timestamp >= ? AND user_id IS NOT NULL |
| ''', (thirty_days_ago,)) |
| active_30d = cursor.fetchone()['count'] |
| |
| |
| cursor.execute(''' |
| SELECT COUNT(*) as count FROM users |
| WHERE registration_date >= ? |
| ''', (seven_days_ago,)) |
| new_users_7d = cursor.fetchone()['count'] |
| |
| cursor.execute(''' |
| SELECT COUNT(*) as count FROM users |
| WHERE registration_date >= ? |
| ''', (thirty_days_ago,)) |
| new_users_30d = cursor.fetchone()['count'] |
| |
| return { |
| 'total_users': total_users, |
| 'active_users_7d': active_7d, |
| 'active_users_30d': active_30d, |
| 'new_users_7d': new_users_7d, |
| 'new_users_30d': new_users_30d |
| } |
| |
| def get_feedback_stats(self) -> Dict: |
| """Get feedback statistics""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| |
| |
| total_feedback = self.count_feedback() |
| |
| |
| positive_feedback = self.count_feedback(is_correct=True) |
| |
| |
| negative_feedback = self.count_feedback(is_correct=False) |
| |
| |
| cursor.execute(''' |
| SELECT category, COUNT(*) as count |
| FROM feedback |
| GROUP BY category |
| ''') |
| category_counts = {row['category']: row['count'] for row in cursor.fetchall()} |
| |
| |
| total_recordings = self.count_predictions() |
| |
| return { |
| 'total_feedback': total_feedback, |
| 'positive_feedback': positive_feedback, |
| 'negative_feedback': negative_feedback, |
| 'category_counts': category_counts, |
| 'total_recordings': total_recordings |
| } |
| |
| def get_platform_distribution(self) -> Dict: |
| """Get platform distribution""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute(''' |
| SELECT platform, COUNT(*) as count |
| FROM users |
| GROUP BY platform |
| ''') |
| return {row['platform']: row['count'] for row in cursor.fetchall()} |
| |
| def get_language_distribution(self) -> Dict: |
| """Get language distribution""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| cursor.execute(''' |
| SELECT language, COUNT(*) as count |
| FROM users |
| GROUP BY language |
| ''') |
| return {row['language']: row['count'] for row in cursor.fetchall()} |
| |
| def get_user_timeline(self, days: int = 30) -> List[Dict]: |
| """Get user registration timeline""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| from datetime import timedelta |
| days_ago = (datetime.now() - timedelta(days=days)).isoformat() |
| cursor.execute(''' |
| SELECT DATE(registration_date) as date, COUNT(*) as count |
| FROM users |
| WHERE registration_date >= ? |
| GROUP BY DATE(registration_date) |
| ORDER BY date ASC |
| ''', (days_ago,)) |
| return [{'date': row['date'], 'count': row['count']} for row in cursor.fetchall()] |
| |
| def get_feedback_timeline(self, days: int = 30) -> List[Dict]: |
| """Get feedback submission timeline""" |
| with self.get_connection() as conn: |
| cursor = conn.cursor() |
| from datetime import timedelta |
| days_ago = (datetime.now() - timedelta(days=days)).isoformat() |
| cursor.execute(''' |
| SELECT DATE(timestamp) as date, COUNT(*) as count |
| FROM feedback |
| WHERE timestamp >= ? |
| GROUP BY DATE(timestamp) |
| ORDER BY date ASC |
| ''', (days_ago,)) |
| return [{'date': row['date'], 'count': row['count']} for row in cursor.fetchall()] |
|
|
|
|