ProjetVM/bdd/database.py
2025-12-08 14:18:02 +01:00

356 lines
11 KiB
Python

"""
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)