""" Couche d'abstraction pour la base de données. Pattern similaire aux interfaces Java avec implémentations multiples. """ import os from abc import ABC, abstractmethod from typing import Any, List, Optional, Tuple class DatabaseBackend(ABC): """Interface abstraite pour les backends de base de données (comme une interface Java)""" @abstractmethod def connect(self): """Établit une connexion à la base de données""" pass @abstractmethod def execute(self, query: str, params: Tuple = ()) -> Any: """Exécute une requête et retourne le curseur""" pass @abstractmethod def fetchone(self, query: str, params: Tuple = ()) -> Optional[Tuple]: """Exécute une requête et retourne une ligne""" pass @abstractmethod def fetchall(self, query: str, params: Tuple = ()) -> List[Tuple]: """Exécute une requête et retourne toutes les lignes""" pass @abstractmethod def commit(self): """Commit la transaction en cours""" pass @abstractmethod def close(self): """Ferme la connexion""" pass @abstractmethod def get_placeholder(self) -> str: """Retourne le placeholder pour les requêtes paramétrées""" pass @abstractmethod def recreate_database(self): """Recrée complètement la base de données""" pass @abstractmethod def get_create_users_table_sql(self) -> str: """Retourne le SQL pour créer la table Users""" pass @abstractmethod def get_create_ue_table_sql(self) -> str: """Retourne le SQL pour créer la table UE""" pass @abstractmethod def get_create_matieres_table_sql(self) -> str: """Retourne le SQL pour créer la table Matieres""" pass @abstractmethod def get_create_notes_table_sql(self) -> str: """Retourne le SQL pour créer la table Notes""" pass class SQLiteBackend(DatabaseBackend): """Implémentation SQLite du backend de base de données""" def __init__(self, db_path: str = "cours.db"): self.db_path = db_path self.connection = None self.cursor = None def connect(self): import sqlite3 self.connection = sqlite3.connect(self.db_path) self.cursor = self.connection.cursor() return self.connection def execute(self, query: str, params: Tuple = ()) -> Any: if not self.connection: self.connect() self.cursor.execute(query, params) return self.cursor def fetchone(self, query: str, params: Tuple = ()) -> Optional[Tuple]: self.execute(query, params) return self.cursor.fetchone() def fetchall(self, query: str, params: Tuple = ()) -> List[Tuple]: self.execute(query, params) return self.cursor.fetchall() def commit(self): if self.connection: self.connection.commit() def close(self): if self.cursor: self.cursor.close() if self.connection: self.connection.close() self.connection = None self.cursor = None def get_placeholder(self) -> str: return "?" def recreate_database(self): """Pour SQLite, on supprime juste le fichier""" import os if os.path.exists(self.db_path): os.remove(self.db_path) def get_create_users_table_sql(self) -> str: return """ CREATE TABLE IF NOT EXISTS Users ( id INTEGER PRIMARY KEY AUTOINCREMENT, login VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, role VARCHAR(10) NOT NULL CHECK(role IN ('prof', 'eleve')), nom VARCHAR(100), prenom VARCHAR(100) ) """ def get_create_ue_table_sql(self) -> str: return """ CREATE TABLE IF NOT EXISTS UE ( id INTEGER PRIMARY KEY AUTOINCREMENT, code VARCHAR(20) UNIQUE NOT NULL, nom VARCHAR(100) NOT NULL, description TEXT ) """ def get_create_matieres_table_sql(self) -> str: return """ CREATE TABLE IF NOT EXISTS Matieres ( id INTEGER PRIMARY KEY AUTOINCREMENT, code VARCHAR(20) UNIQUE NOT NULL, nom VARCHAR(100) NOT NULL, ue_id INTEGER NOT NULL, coefficient REAL DEFAULT 1.0, FOREIGN KEY (ue_id) REFERENCES UE(id) ON DELETE CASCADE ) """ def get_create_notes_table_sql(self) -> str: return """ CREATE TABLE IF NOT EXISTS Notes ( id INTEGER PRIMARY KEY AUTOINCREMENT, eleve_id INTEGER NOT NULL, matiere_id INTEGER NOT NULL, note REAL NOT NULL CHECK(note >= 0 AND note <= 20), date_creation TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (eleve_id) REFERENCES Users(id) ON DELETE CASCADE, FOREIGN KEY (matiere_id) REFERENCES Matieres(id) ON DELETE CASCADE ) """ class MariaDBBackend(DatabaseBackend): """Implémentation MariaDB/MySQL du backend de base de données""" def __init__( self, host: str = "mariadb", user: str = "root", password: str = "un-bon-mdp-solide", database: str = "APPNOTE", ): self.config = { "host": host, "user": user, "password": password, "database": database, } self.database_name = database self.connection = None self.cursor = None def connect(self): import mysql.connector self.connection = mysql.connector.connect(**self.config) self.cursor = self.connection.cursor() return self.connection def execute(self, query: str, params: Tuple = ()) -> Any: if not self.connection: self.connect() self.cursor.execute(query, params) return self.cursor def fetchone(self, query: str, params: Tuple = ()) -> Optional[Tuple]: self.execute(query, params) return self.cursor.fetchone() def fetchall(self, query: str, params: Tuple = ()) -> List[Tuple]: self.execute(query, params) return self.cursor.fetchall() def commit(self): if self.connection: self.connection.commit() def close(self): if self.cursor: self.cursor.close() if self.connection: self.connection.close() self.connection = None self.cursor = None def get_placeholder(self) -> str: return "%s" def recreate_database(self): """Pour MariaDB, on drop/create la base""" import mysql.connector # Connexion sans spécifier la base config_without_db = { "host": self.config["host"], "user": self.config["user"], "password": self.config["password"], } con = mysql.connector.connect(**config_without_db) cur = con.cursor() cur.execute(f"DROP DATABASE IF EXISTS {self.database_name}") cur.execute(f"CREATE DATABASE {self.database_name}") cur.execute(f"USE {self.database_name}") con.commit() cur.close() con.close() def get_create_users_table_sql(self) -> str: return """ CREATE TABLE Users ( id INT AUTO_INCREMENT PRIMARY KEY, login VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, role ENUM('prof', 'eleve') NOT NULL, nom VARCHAR(100), prenom VARCHAR(100) ) """ def get_create_ue_table_sql(self) -> str: return """ CREATE TABLE UE ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) UNIQUE NOT NULL, nom VARCHAR(100) NOT NULL, description TEXT ) """ def get_create_matieres_table_sql(self) -> str: return """ CREATE TABLE Matieres ( id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) UNIQUE NOT NULL, nom VARCHAR(100) NOT NULL, ue_id INT NOT NULL, coefficient DECIMAL(5,2) DEFAULT 1.0, FOREIGN KEY (ue_id) REFERENCES UE(id) ON DELETE CASCADE ) """ def get_create_notes_table_sql(self) -> str: return """ CREATE TABLE Notes ( id INT AUTO_INCREMENT PRIMARY KEY, eleve_id INT NOT NULL, matiere_id INT NOT NULL, note DECIMAL(4,2) NOT NULL CHECK(note >= 0 AND note <= 20), date_creation TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (eleve_id) REFERENCES Users(id) ON DELETE CASCADE, FOREIGN KEY (matiere_id) REFERENCES Matieres(id) ON DELETE CASCADE ) """ class Database: """Wrapper pour le backend avec context manager (pattern Facade)""" def __init__(self, backend: DatabaseBackend): self.backend = backend def execute(self, query: str, params: Tuple = ()) -> Any: """Exécute une requête""" return self.backend.execute(query, params) def fetchone(self, query: str, params: Tuple = ()) -> Optional[Tuple]: """Exécute et retourne une ligne""" return self.backend.fetchone(query, params) def fetchall(self, query: str, params: Tuple = ()) -> List[Tuple]: """Exécute et retourne toutes les lignes""" return self.backend.fetchall(query, params) def commit(self): """Commit la transaction""" self.backend.commit() def close(self): """Ferme la connexion""" self.backend.close() def __enter__(self): """Support du context manager""" self.backend.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): """Ferme automatiquement la connexion""" if exc_type is None: self.commit() self.close() def get_database() -> Database: """ Factory pour créer le bon backend selon la configuration. Pattern Factory Method. """ db_type = os.getenv("DB_TYPE", "mariadb").lower() if db_type == "sqlite": db_path = os.getenv("SQLITE_DB_PATH", "cours.db") backend = SQLiteBackend(db_path) else: # mariadb par défaut host = os.getenv("DB_HOST", "mariadb") user = os.getenv("DB_USER", "root") password = os.getenv("DB_PASSWORD", "caca") database = os.getenv("DB_NAME", "APPNOTE") backend = MariaDBBackend(host, user, password, database) return Database(backend)