Formation Python & Apache Cassandra

Bases de Données Distribuées NoSQL - Année 2025-2026

Projet : Système de Gestion de Bibliothèque Numérique

Python • Apache Cassandra • Architecture Distribuée

⏱️ Durée : 1 semaine

🎯 Contexte du Projet

Vous êtes une équipe de développeurs chargée de concevoir et implémenter un système de gestion de bibliothèque numérique capable de gérer des millions de livres, d'utilisateurs et d'emprunts.

Le système doit être hautement disponible et scalable horizontalement pour supporter une charge importante. Vous utiliserez Apache Cassandra, une base de données NoSQL distribuée idéale pour ce type de scénario, et Python pour développer l'application.

Cas d'usage réel : Bibliothèque universitaire avec 50,000+ étudiants, 100,000+ livres, gestion des emprunts, réservations, et historique de lecture.

⚠️ Prérequis Techniques

Ce que vous allez apprendre :

📚 Comprendre Apache Cassandra

🔑 Partition Key

Détermine sur quel nœud les données sont stockées. Choisir une bonne partition key est CRUCIAL pour les performances.

Exemple : user_id pour une table users

🔢 Clustering Key

Ordonne les données au sein d'une partition. Permet des requêtes par plage.

Exemple : borrow_date pour trier les emprunts

📊 Modélisation par Requêtes

Contrairement au SQL, on crée une table PAR type de requête nécessaire. La dénormalisation est normale.

Règle : 1 query pattern = 1 table

⚖️ Consistency vs Availability

Cassandra privilégie la disponibilité (AP dans CAP theorem). On peut ajuster le niveau de cohérence par requête.

Niveaux : ONE, QUORUM, ALL

⛔ Anti-Patterns Cassandra à ÉVITER

  • ❌ Requêtes sans partition key → Scan complet du cluster (très lent)
  • ❌ Large partitions (>100MB) → Performance dégradée
  • ❌ ALLOW FILTERING → Indique un mauvais modèle de données
  • ❌ Utiliser Cassandra comme SQL → Pas de JOIN, pas de subquery
  • ❌ Trop de tables secondaires → Complexité de maintenance

🏗️ Architecture du Système

┌─────────────────────────────────────────────────────┐ │ ARCHITECTURE BIBLIOTHÈQUE │ └─────────────────────────────────────────────────────┘ ┌──────────────────────┐ │ PYTHON APP │ │ • Flask API │ │ • CLI Interface │ │ • Business Logic │ └──────────┬───────────┘ │ │ cassandra-driver │ ┌──────────▼───────────────────────────────────┐ │ APACHE CASSANDRA CLUSTER │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ Node 1 │ │ Node 2 │ │ Node 3 │ │ │ │ (seed) │ │ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ Keyspace: library_system │ │ Replication Factor: 3 │ └──────────────────────────────────────────────┘ Tables Cassandra: ├── books_by_id (recherche par ISBN) ├── books_by_category (navigation par catégorie) ├── books_by_author (recherche par auteur) ├── users_by_id (profil utilisateur) ├── borrows_by_user (historique emprunts) ├── borrows_by_book (qui a emprunté ce livre) ├── reservations (files d'attente) └── statistics (compteurs temps réel)

📋 Modèle de Données Cassandra

Query Patterns (Requêtes à supporter)

  1. Trouver un livre par ISBN
  2. Lister tous les livres d'une catégorie
  3. Trouver tous les livres d'un auteur
  4. Voir le profil d'un utilisateur
  5. Historique des emprunts d'un utilisateur
  6. Qui a emprunté un livre spécifique ?
  7. Livres actuellement empruntés (non retournés)
  8. Réservations en attente pour un livre
  9. Statistiques globales (nb emprunts, livres populaires)

🔧 Installation et Configuration

1Installation Cassandra avec Docker

Objectif : Mettre en place un cluster Cassandra local avec 3 nœuds.
# docker-compose.yml version: '3.8' services: cassandra-node1: image: cassandra:4.1 container_name: cassandra1 hostname: cassandra1 ports: - "9042:9042" # CQL native port - "9160:9160" # Thrift port environment: - CASSANDRA_SEEDS=cassandra1 - CASSANDRA_CLUSTER_NAME=LibraryCluster - CASSANDRA_DC=datacenter1 - CASSANDRA_RACK=rack1 - CASSANDRA_ENDPOINT_SNITCH=GossipingPropertyFileSnitch volumes: - cassandra1_data:/var/lib/cassandra networks: - cassandra_network healthcheck: test: ["CMD-SHELL", "nodetool status"] interval: 30s timeout: 10s retries: 5 cassandra-node2: image: cassandra:4.1 container_name: cassandra2 hostname: cassandra2 environment: - CASSANDRA_SEEDS=cassandra1 - CASSANDRA_CLUSTER_NAME=LibraryCluster - CASSANDRA_DC=datacenter1 - CASSANDRA_RACK=rack1 volumes: - cassandra2_data:/var/lib/cassandra networks: - cassandra_network depends_on: cassandra-node1: condition: service_healthy cassandra-node3: image: cassandra:4.1 container_name: cassandra3 hostname: cassandra3 environment: - CASSANDRA_SEEDS=cassandra1 - CASSANDRA_CLUSTER_NAME=LibraryCluster - CASSANDRA_DC=datacenter1 - CASSANDRA_RACK=rack1 volumes: - cassandra3_data:/var/lib/cassandra networks: - cassandra_network depends_on: cassandra-node1: condition: service_healthy volumes: cassandra1_data: cassandra2_data: cassandra3_data: networks: cassandra_network: driver: bridge # Commandes: # docker-compose up -d # docker exec -it cassandra1 cqlsh # docker-compose logs -f cassandra-node1

Vérifier le cluster :

# Dans le container docker exec -it cassandra1 nodetool status # Résultat attendu: 3 nœuds UP (UN) Datacenter: datacenter1 ======================= Status=Up/Down |/ State=Normal/Leaving/Joining/Moving -- Address Load Tokens Owns Host ID UN 172.18.0.2 103.71 KiB 256 100.0% abc123... UN 172.18.0.3 98.45 KiB 256 100.0% def456... UN 172.18.0.4 101.32 KiB 256 100.0% ghi789...

2Configuration Python

Objectif : Installer les dépendances Python et tester la connexion.
# requirements.txt cassandra-driver==3.28.0 faker==20.1.0 # Générer données de test python-dotenv==1.0.0 loguru==0.7.2 click==8.1.7 # Pour CLI tabulate==0.9.0 # Tables ASCII colorama==0.4.6 # Couleurs terminal # Installation pip install -r requirements.txt
# config/database.py from cassandra.cluster import Cluster from cassandra.auth import PlainTextAuthProvider from loguru import logger class CassandraConnection: """Gestionnaire de connexion Cassandra""" def __init__(self, hosts=['127.0.0.1'], port=9042, keyspace='library_system'): self.hosts = hosts self.port = port self.keyspace = keyspace self.cluster = None self.session = None def connect(self): """Établir la connexion""" try: self.cluster = Cluster( contact_points=self.hosts, port=self.port ) self.session = self.cluster.connect() logger.success(f"✅ Connecté à Cassandra: {self.hosts}") # Utiliser le keyspace self.session.set_keyspace(self.keyspace) logger.success(f"✅ Keyspace actif: {self.keyspace}") return self.session except Exception as e: logger.error(f"❌ Erreur connexion: {e}") raise def close(self): """Fermer la connexion""" if self.cluster: self.cluster.shutdown() logger.info("🔌 Connexion fermée") # Test de connexion if __name__ == "__main__": db = CassandraConnection() session = db.connect() # Test query rows = session.execute("SELECT release_version FROM system.local") for row in rows: logger.info(f"Cassandra version: {row.release_version}") db.close()

3Initialisation du Schéma

Objectif : Créer le keyspace et toutes les tables.
# scripts/init_schema.py from config.database import CassandraConnection from loguru import logger def create_keyspace(session): """Créer le keyspace""" query = """ CREATE KEYSPACE IF NOT EXISTS library_system WITH replication = { 'class': 'SimpleStrategy', 'replication_factor': 3 } """ session.execute(query) logger.success("✅ Keyspace créé") def create_tables(session): """Créer toutes les tables""" session.set_keyspace('library_system') # Lire le fichier schema.cql with open('schema/schema.cql', 'r') as f: cql_commands = f.read() # Exécuter chaque commande for command in cql_commands.split(';'): command = command.strip() if command: try: session.execute(command) logger.success(f"✅ Table créée") except Exception as e: logger.warning(f"⚠️ {e}") if __name__ == "__main__": db = CassandraConnection(keyspace='system') session = db.connect() create_keyspace(session) create_tables(session) logger.success("🎉 Schéma initialisé!") db.close()

💻 Développement de l'Application

4Module Books - Gestion des Livres

Objectif : Implémenter les opérations CRUD sur les livres.

5Module Users - Gestion des Utilisateurs

Objectif : Gérer les profils utilisateurs.

6Module Borrows - Gestion des Emprunts

Objectif : Implémenter la logique d'emprunt et de retour.

7CLI - Interface en Ligne de Commande

Objectif : Créer une interface utilisateur conviviale.
# cli/main.py import click from uuid import UUID from tabulate import tabulate from config.database import CassandraConnection from models.book import BookRepository, Book from models.user import UserRepository from models.borrow import BorrowRepository # Connexion globale db = CassandraConnection() session = db.connect() book_repo = BookRepository(session) user_repo = UserRepository(session) borrow_repo = BorrowRepository(session) @click.group() def cli(): """📚 Système de Gestion de Bibliothèque""" pass # ========== BOOKS ========== @cli.group() def books(): """Gestion des livres""" pass @books.command() @click.option('--isbn', prompt='ISBN', help='ISBN du livre') @click.option('--title', prompt='Titre', help='Titre du livre') @click.option('--author', prompt='Auteur', help='Auteur') @click.option('--category', prompt='Catégorie', help='Catégorie') @click.option('--publisher', prompt='Éditeur', help='Éditeur') @click.option('--year', prompt='Année', type=int, help='Année de publication') @click.option('--copies', prompt='Nombre de copies', type=int, default=1) def add(isbn, title, author, category, publisher, year, copies): """Ajouter un livre""" book = Book( isbn=isbn, title=title, author=author, category=category, publisher=publisher, publication_year=year, total_copies=copies, available_copies=copies ) if book_repo.add_book(book): click.echo(click.style(f"✅ Livre ajouté: {title}", fg='green')) else: click.echo(click.style(f"❌ Erreur", fg='red')) @books.command() @click.option('--isbn', prompt='ISBN', help='ISBN du livre') def search(isbn): """Rechercher un livre par ISBN""" book = book_repo.get_book_by_isbn(isbn) if book: data = [ ["ISBN", book.isbn], ["Titre", book.title], ["Auteur", book.author], ["Catégorie", book.category], ["Année", book.publication_year], ["Copies dispo", f"{book.available_copies}/{book.total_copies}"] ] click.echo("\n" + tabulate(data, tablefmt="grid")) else: click.echo(click.style("❌ Livre introuvable", fg='red')) @books.command() @click.option('--category', prompt='Catégorie', help='Catégorie') def list_by_category(category): """Lister les livres d'une catégorie""" books = book_repo.get_books_by_category(category) if books: data = [[b['isbn'], b['title'], b['author'], b['available_copies']] for b in books] headers = ['ISBN', 'Titre', 'Auteur', 'Disponibles'] click.echo("\n" + tabulate(data, headers=headers, tablefmt="grid")) else: click.echo(click.style("Aucun livre trouvé", fg='yellow')) # ========== USERS ========== @cli.group() def users(): """Gestion des utilisateurs""" pass @users.command() @click.option('--email', prompt='Email', help='Email') @click.option('--first-name', prompt='Prénom', help='Prénom') @click.option('--last-name', prompt='Nom', help='Nom') def register(email, first_name, last_name): """Inscrire un utilisateur""" user_id = user_repo.create_user(email, first_name, last_name) click.echo(click.style(f"✅ Utilisateur créé: {user_id}", fg='green')) @users.command() @click.option('--user-id', prompt='User ID', help='UUID de l\'utilisateur') def profile(user_id): """Voir le profil d'un utilisateur""" user = user_repo.get_user(UUID(user_id)) if user: data = [ ["ID", user.user_id], ["Nom", f"{user.first_name} {user.last_name}"], ["Email", user.email], ["Inscription", user.registration_date], ["Emprunts totaux", user.total_borrows], ["Emprunts actifs", user.active_borrows] ] click.echo("\n" + tabulate(data, tablefmt="grid")) else: click.echo(click.style("❌ Utilisateur introuvable", fg='red')) # ========== BORROWS ========== @cli.group() def borrows(): """Gestion des emprunts""" pass @borrows.command() @click.option('--user-id', prompt='User ID', help='UUID de l\'utilisateur') @click.option('--isbn', prompt='ISBN', help='ISBN du livre') def borrow(user_id, isbn): """Emprunter un livre""" # Récupérer les infos user = user_repo.get_user(UUID(user_id)) book = book_repo.get_book_by_isbn(isbn) if not user: click.echo(click.style("❌ Utilisateur introuvable", fg='red')) return if not book: click.echo(click.style("❌ Livre introuvable", fg='red')) return user_name = f"{user.first_name} {user.last_name}" if borrow_repo.borrow_book(user.user_id, isbn, book.title, user_name): click.echo(click.style(f"✅ Emprunt réussi: {book.title}", fg='green')) else: click.echo(click.style("❌ Emprunt échoué", fg='red')) @borrows.command() @click.option('--user-id', prompt='User ID', help='UUID de l\'utilisateur') @click.option('--isbn', prompt='ISBN', help='ISBN du livre') def return_book(user_id, isbn): """Retourner un livre""" if borrow_repo.return_book(UUID(user_id), isbn): click.echo(click.style("✅ Livre retourné", fg='green')) else: click.echo(click.style("❌ Erreur", fg='red')) @borrows.command() @click.option('--user-id', prompt='User ID', help='UUID de l\'utilisateur') def history(user_id): """Historique des emprunts""" borrows = borrow_repo.get_user_borrows(UUID(user_id)) if borrows: data = [[b['isbn'], b['book_title'], b['borrow_date'], b['status']] for b in borrows] headers = ['ISBN', 'Titre', 'Date', 'Statut'] click.echo("\n" + tabulate(data, headers=headers, tablefmt="grid")) else: click.echo(click.style("Aucun emprunt", fg='yellow')) if __name__ == '__main__': try: cli() finally: db.close()

Utilisation du CLI :

# Ajouter un livre python cli/main.py books add # Rechercher un livre python cli/main.py books search --isbn "978-0-123456-78-9" # Lister par catégorie python cli/main.py books list-by-category --category "Science Fiction" # Inscrire un utilisateur python cli/main.py users register # Emprunter python cli/main.py borrows borrow # Retourner python cli/main.py borrows return-book # Historique python cli/main.py borrows history --user-id "abc-123-def"

🧪 Tests et Données de Démonstration

8Génération de Données de Test

Objectif : Peupler la base avec des données réalistes.
# scripts/generate_data.py from faker import Faker from uuid import uuid4 from random import randint, choice from models.book import BookRepository, Book from models.user import UserRepository from config.database import CassandraConnection from loguru import logger fake = Faker('fr_FR') def generate_books(book_repo, count=100): """Générer des livres aléatoires""" categories = ['Science Fiction', 'Fantasy', 'Thriller', 'Romance', 'Histoire', 'Science', 'Biographie', 'Philosophie'] publishers = ['Gallimard', 'Flammarion', 'Hachette', 'Albin Michel', 'Seuil'] logger.info(f"🎲 Génération de {count} livres...") for i in range(count): isbn = f"978-{randint(0,9)}-{randint(100000,999999)}-{randint(10,99)}-{randint(0,9)}" book = Book( isbn=isbn, title=fake.sentence(nb_words=4)[:-1], # Titre aléatoire author=fake.name(), category=choice(categories), publisher=choice(publishers), publication_year=randint(1950, 2024), total_copies=randint(1, 10), available_copies=randint(0, 10), description=fake.text(max_nb_chars=200) ) book_repo.add_book(book) if (i + 1) % 10 == 0: logger.info(f" ✅ {i+1}/{count} livres créés") logger.success(f"✅ {count} livres générés!") def generate_users(user_repo, count=50): """Générer des utilisateurs aléatoires""" logger.info(f"👥 Génération de {count} utilisateurs...") user_ids = [] for i in range(count): email = fake.email() first_name = fake.first_name() last_name = fake.last_name() phone = fake.phone_number() address = fake.address().replace('\n', ', ') user_id = user_repo.create_user( email=email, first_name=first_name, last_name=last_name, phone=phone, address=address ) user_ids.append(user_id) if (i + 1) % 10 == 0: logger.info(f" ✅ {i+1}/{count} utilisateurs créés") logger.success(f"✅ {count} utilisateurs générés!") return user_ids if __name__ == "__main__": db = CassandraConnection() session = db.connect() book_repo = BookRepository(session) user_repo = UserRepository(session) # Générer des données generate_books(book_repo, count=100) user_ids = generate_users(user_repo, count=50) logger.success("🎉 Base de données peuplée!") db.close()

📦 Livrables Attendus

1. Code Source Complet

Repository Git organisé avec:

  • Tous les modules Python (config, models, cli)
  • Fichier schema.cql avec toutes les tables
  • docker-compose.yml fonctionnel
  • requirements.txt
  • Scripts de génération de données

2. Documentation Technique

  • README.md : Instructions installation et utilisation
  • ARCHITECTURE.md : Explication du modèle de données
  • QUERIES.md : Liste des query patterns et justifications
  • Diagrammes (schéma tables, flux de données)

3. Rapport d'Analyse (PDF)

Document de 5-8 pages avec:

  • Choix de modélisation Cassandra (pourquoi ces tables?)
  • Partition keys et clustering keys justifiés
  • Trade-offs entre cohérence et disponibilité
  • Comparaison avec une approche SQL traditionnelle
  • Tests de performance (optionnel)

4. Démonstration

  • Vidéo (5-10 min) ou présentation live
  • Montrer le cluster Cassandra (nodetool status)
  • Démonstration du CLI (CRUD complet)
  • Exemples de requêtes CQL avancées

📊 Barème de Notation (100 points)

1. Modélisation Cassandra 25 pts
2. Implémentation Python 30 pts
3. Interface CLI 15 pts
4. Infrastructure Docker 10 pts
5. Documentation 10 pts
6. Qualité du Code 10 pts

BONUS (+20 points max)
• +10 pts: Tests unitaires (pytest) avec bonne couverture
• +8 pts: API REST Flask/FastAPI en plus du CLI
• +7 pts: Interface web (Streamlit/Flask)
• +5 pts: Système de réservations fonctionnel
• +5 pts: Benchmark de performance (lecture/écriture)

💡 Conseils et Bonnes Pratiques

✅ Modélisation Cassandra

  • Commencez par les requêtes : Listez TOUS les query patterns avant de créer les tables
  • Une table = un query pattern : Ne cherchez pas à tout mettre dans une seule table
  • Partition key = clé de distribution : Doit distribuer uniformément les données
  • Évitez les large partitions : >100MB par partition = problème
  • Dénormalisez : C'est normal d'avoir des données dupliquées dans Cassandra

✅ Python + Cassandra

  • Utilisez prepared statements : Plus rapides et sécurisés
  • Batch avec parcimonie : BATCH n'est pas une transaction, uniquement pour même partition
  • Async si nécessaire : cassandra-driver supporte asyncio
  • Connection pooling : Réutilisez la session, ne créez pas une connexion par requête
  • Gestion d'erreurs : try/except sur toutes les opérations Cassandra

⚠️ Pièges à Éviter

  • ❌ SELECT * sans WHERE : Scan de tout le cluster
  • ❌ ALLOW FILTERING : Signe d'un mauvais modèle
  • ❌ Penser en SQL : Oubliez les JOIN, subqueries, etc.
  • ❌ Index secondaires partout : À utiliser avec parcimonie
  • ❌ UPDATE sans WHERE : Impossible dans Cassandra
  • ❌ Attendre ACID : Cassandra privilégie disponibilité sur cohérence

📚 Ressources

⏰ Planning Suggéré (1 semaine)

Jour 1: Setup & Schéma

Docker, Cassandra cluster, définir les query patterns, créer le schéma CQL

Jour 2: Module Books

BookRepository complet avec toutes les opérations CRUD

Jour 3: Module Users + Borrows

UserRepository et BorrowRepository, logique métier

Jour 4: CLI

Interface utilisateur complète avec Click

Jour 5: Tests & Données

Génération de données, tests manuels, fix bugs

Jour 6: Documentation

README, rapport technique, diagrammes

Jour 7: Finalisation

Démo, présentation, dernières retouches