Projet complet
De la conception à l'implémentation
Créer un projet Python complet
Félicitations ! Vous avez parcouru tous les tutoriels précédents et acquis une solide base de connaissances en Python. Il est maintenant temps de mettre en pratique tout ce que vous avez appris en développant un projet complet de bout en bout.
Dans ce tutoriel, nous allons construire une application de gestion de tâches (todo list) qui utilisera une interface en ligne de commande. Ce projet intégrera la plupart des concepts que nous avons abordés dans cette série de tutoriels, notamment :
- La programmation orientée objet
- La gestion des exceptions
- La manipulation de fichiers JSON
- Les structures de contrôle
- Les bibliothèques standard de Python
- Les bonnes pratiques de développement
Préparation du projet
Structure du projet
Commençons par créer une structure de projet propre et organisée :
todo_app/
│
├── todo/
│ ├── __init__.py
│ ├── models.py # Classes de données (Task, TaskList)
│ ├── storage.py # Gestion du stockage (JSON)
│ ├── commands.py # Implémentation des commandes
│ └── cli.py # Interface ligne de commande
│
├── tests/ # Tests unitaires
│ ├── __init__.py
│ ├── test_models.py
│ └── test_storage.py
│
├── data/ # Dossier pour stocker les données
│ └── tasks.json
│
├── requirements.txt # Dépendances du projet
├── setup.py # Configuration d'installation
└── README.md # Documentation du projet
Mise en place de l’environnement virtuel
Créons un environnement virtuel pour notre projet :
# Création de l'environnement virtuel
python -m venv venv
# Activation de l'environnement virtuel
# Sur Windows
venv\Scripts\activate
# Sur macOS/Linux
source venv/bin/activate
# Installation des dépendances
pip install -e .
Définition des dépendances
Créons un fichier requirements.txt
avec les dépendances nécessaires :
# requirements.txt
colorama>=0.4.4 # Pour la coloration dans le terminal
pytest>=6.2.5 # Pour les tests unitaires
Et un fichier setup.py
pour rendre notre package installable :
# setup.py
from setuptools import setup, find_packages
setup(
name="todo-app",
version="0.1.0",
packages=find_packages(),
install_requires=[
"colorama>=0.4.4",
],
entry_points={
"console_scripts": [
"todo=todo.cli:main",
],
},
)
Développement du modèle de données
Commençons par définir nos classes de modèle dans todo/models.py
:
# todo/models.py
from datetime import datetime
from enum import Enum, auto
from typing import List, Optional, Dict, Any
import uuid
class TaskStatus(Enum):
"""Enum représentant les statuts possibles d'une tâche."""
TODO = auto()
IN_PROGRESS = auto()
DONE = auto()
def __str__(self) -> str:
return self.name.replace("_", " ").title()
class Task:
"""Classe représentant une tâche."""
def __init__(self, title: str, description: str = "",
status: TaskStatus = TaskStatus.TODO, due_date: Optional[datetime] = None,
tags: Optional[List[str]] = None, task_id: Optional[str] = None):
self.title = title
self.description = description
self.status = status
self.created_at = datetime.now()
self.updated_at = self.created_at
self.due_date = due_date
self.tags = tags or []
self.id = task_id or str(uuid.uuid4())
def update(self, **kwargs) -> None:
"""Met à jour les attributs de la tâche."""
for key, value in kwargs.items():
if hasattr(self, key) and key not in ["id", "created_at"]:
setattr(self, key, value)
self.updated_at = datetime.now()
def to_dict(self) -> Dict[str, Any]:
"""Convertit la tâche en dictionnaire pour la sérialisation."""
return {
"id": self.id,
"title": self.title,
"description": self.description,
"status": self.status.name,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
"due_date": self.due_date.isoformat() if self.due_date else None,
"tags": self.tags
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Task':
"""Crée une instance de Task à partir d'un dictionnaire."""
status = TaskStatus[data["status"]]
due_date = datetime.fromisoformat(data["due_date"]) if data["due_date"] else None
task = cls(
title=data["title"],
description=data["description"],
status=status,
due_date=due_date,
tags=data["tags"],
task_id=data["id"]
)
task.created_at = datetime.fromisoformat(data["created_at"])
task.updated_at = datetime.fromisoformat(data["updated_at"])
return task
def __str__(self) -> str:
due_str = f", échéance: {self.due_date.strftime('%d/%m/%Y')}" if self.due_date else ""
tags_str = f", tags: {', '.join(self.tags)}" if self.tags else ""
return f"{self.title} ({self.status}){due_str}{tags_str}"
class TaskList:
"""Classe représentant une liste de tâches."""
def __init__(self):
self.tasks: Dict[str, Task] = {}
def add_task(self, task: Task) -> None:
"""Ajoute une tâche à la liste."""
if task.id in self.tasks:
raise ValueError(f"Une tâche avec l'ID {task.id} existe déjà")
self.tasks[task.id] = task
def get_task(self, task_id: str) -> Task:
"""Récupère une tâche par son ID."""
if task_id not in self.tasks:
raise KeyError(f"Aucune tâche trouvée avec l'ID {task_id}")
return self.tasks[task_id]
def update_task(self, task_id: str, **kwargs) -> Task:
"""Met à jour une tâche existante."""
task = self.get_task(task_id)
task.update(**kwargs)
return task
def delete_task(self, task_id: str) -> None:
"""Supprime une tâche de la liste."""
if task_id not in self.tasks:
raise KeyError(f"Aucune tâche trouvée avec l'ID {task_id}")
del self.tasks[task_id]
def filter_tasks(self, status: Optional[TaskStatus] = None,
tag: Optional[str] = None) -> List[Task]:
"""Filtre les tâches par statut et/ou tag."""
filtered_tasks = list(self.tasks.values())
if status:
filtered_tasks = [task for task in filtered_tasks if task.status == status]
if tag:
filtered_tasks = [task for task in filtered_tasks if tag in task.tags]
return filtered_tasks
def to_dict(self) -> Dict[str, Any]:
"""Convertit la liste de tâches en dictionnaire pour la sérialisation."""
return {
"tasks": [task.to_dict() for task in self.tasks.values()]
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TaskList':
"""Crée une instance de TaskList à partir d'un dictionnaire."""
task_list = cls()
for task_data in data.get("tasks", []):
task = Task.from_dict(task_data)
task_list.tasks[task.id] = task
return task_list
Gestion du stockage
Maintenant, créons le module de stockage pour persister nos tâches en JSON :
# todo/storage.py
import json
import os
from pathlib import Path
from typing import Optional
from .models import TaskList
class StorageError(Exception):
"""Exception levée lors d'erreurs de stockage."""
pass
class Storage:
"""Classe gérant le stockage des tâches en JSON."""
def __init__(self, file_path: Optional[str] = None):
if file_path:
self.file_path = Path(file_path)
else:
# Chemin par défaut
data_dir = Path(__file__).parent.parent / "data"
data_dir.mkdir(exist_ok=True)
self.file_path = data_dir / "tasks.json"
def save(self, task_list: TaskList) -> None:
"""Sauvegarde la liste de tâches dans le fichier JSON."""
try:
with open(self.file_path, 'w', encoding='utf-8') as f:
json.dump(task_list.to_dict(), f, indent=2, ensure_ascii=False)
except (IOError, PermissionError) as e:
raise StorageError(f"Erreur lors de la sauvegarde des tâches : {e}")
def load(self) -> TaskList:
"""Charge la liste de tâches depuis le fichier JSON."""
if not self.file_path.exists():
return TaskList()
try:
with open(self.file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
return TaskList.from_dict(data)
except json.JSONDecodeError as e:
raise StorageError(f"Format JSON invalide : {e}")
except (IOError, PermissionError) as e:
raise StorageError(f"Erreur lors du chargement des tâches : {e}")
Implémentation des commandes
Créons le module de commandes pour gérer les opérations sur les tâches :
# todo/commands.py
from datetime import datetime
from typing import List, Optional, Tuple
from .models import Task, TaskList, TaskStatus
from .storage import Storage, StorageError
class CommandError(Exception):
"""Exception levée lors d'erreurs de commande."""
pass
class TodoCommands:
"""Classe implémentant les commandes de l'application."""
def __init__(self, storage: Optional[Storage] = None):
self.storage = storage or Storage()
try:
self.task_list = self.storage.load()
except StorageError as e:
# En cas d'erreur, on commence avec une liste vide
print(f"Attention : {e}")
self.task_list = TaskList()
def _save(self) -> None:
"""Sauvegarde l'état actuel des tâches."""
try:
self.storage.save(self.task_list)
except StorageError as e:
raise CommandError(f"Erreur de sauvegarde : {e}")
def add(self, title: str, description: str = "",
due_date: Optional[str] = None, tags: Optional[str] = None) -> Task:
"""Ajoute une nouvelle tâche."""
# Validation des entrées
if not title:
raise CommandError("Le titre ne peut pas être vide")
# Conversion de la date d'échéance
parsed_due_date = None
if due_date:
try:
parsed_due_date = datetime.strptime(due_date, "%d/%m/%Y")
except ValueError:
raise CommandError("Format de date invalide. Utilisez JJ/MM/AAAA")
# Traitement des tags
tag_list = []
if tags:
tag_list = [tag.strip() for tag in tags.split(',')]
# Création et ajout de la tâche
new_task = Task(
title=title,
description=description,
due_date=parsed_due_date,
tags=tag_list
)
try:
self.task_list.add_task(new_task)
self._save()
return new_task
except ValueError as e:
raise CommandError(str(e))
def list(self, status: Optional[str] = None,
tag: Optional[str] = None) -> List[Task]:
"""Liste les tâches avec filtrage optionnel."""
status_enum = None
if status:
try:
status_enum = TaskStatus[status.upper()]
except KeyError:
raise CommandError(f"Statut invalide : {status}")
return self.task_list.filter_tasks(status=status_enum, tag=tag)
def update(self, task_id: str, **kwargs) -> Task:
"""Met à jour une tâche existante."""
# Traitement spécial pour certains champs
if 'due_date' in kwargs and kwargs['due_date']:
try:
kwargs['due_date'] = datetime.strptime(kwargs['due_date'], "%d/%m/%Y")
except ValueError:
raise CommandError("Format de date invalide. Utilisez JJ/MM/AAAA")
if 'status' in kwargs and kwargs['status']:
try:
kwargs['status'] = TaskStatus[kwargs['status'].upper()]
except KeyError:
raise CommandError(f"Statut invalide : {kwargs['status']}")
if 'tags' in kwargs and kwargs['tags']:
kwargs['tags'] = [tag.strip() for tag in kwargs['tags'].split(',')]
try:
task = self.task_list.update_task(task_id, **kwargs)
self._save()
return task
except KeyError:
raise CommandError(f"Aucune tâche trouvée avec l'ID {task_id}")
def delete(self, task_id: str) -> None:
"""Supprime une tâche."""
try:
self.task_list.delete_task(task_id)
self._save()
except KeyError:
raise CommandError(f"Aucune tâche trouvée avec l'ID {task_id}")
def get(self, task_id: str) -> Task:
"""Récupère les détails d'une tâche spécifique."""
try:
return self.task_list.get_task(task_id)
except KeyError:
raise CommandError(f"Aucune tâche trouvée avec l'ID {task_id}")
Interface en ligne de commande
Enfin, créons l’interface ligne de commande dans todo/cli.py
:
# todo/cli.py
import argparse
import sys
from typing import List
from datetime import datetime
import colorama
from colorama import Fore, Style
from .commands import TodoCommands, CommandError
from .models import TaskStatus
def setup_argparse() -> argparse.ArgumentParser:
"""Configure le parser d'arguments."""
parser = argparse.ArgumentParser(
description="Application de gestion de tâches en ligne de commande"
)
subparsers = parser.add_subparsers(dest="command", help="Commandes disponibles")
# Commande "add"
add_parser = subparsers.add_parser("add", help="Ajouter une nouvelle tâche")
add_parser.add_argument("title", help="Titre de la tâche")
add_parser.add_argument("-d", "--description", help="Description de la tâche")
add_parser.add_argument("--due", help="Date d'échéance (format JJ/MM/AAAA)")
add_parser.add_argument("--tags", help="Tags séparés par des virgules")
# Commande "list"
list_parser = subparsers.add_parser("list", help="Lister les tâches")
list_parser.add_argument("-s", "--status", help="Filtrer par statut (TODO, IN_PROGRESS, DONE)")
list_parser.add_argument("-t", "--tag", help="Filtrer par tag")
# Commande "update"
update_parser = subparsers.add_parser("update", help="Mettre à jour une tâche")
update_parser.add_argument("id", help="ID de la tâche à mettre à jour")
update_parser.add_argument("-t", "--title", help="Nouveau titre")
update_parser.add_argument("-d", "--description", help="Nouvelle description")
update_parser.add_argument("-s", "--status", help="Nouveau statut (TODO, IN_PROGRESS, DONE)")
update_parser.add_argument("--due", help="Nouvelle date d'échéance (format JJ/MM/AAAA)")
update_parser.add_argument("--tags", help="Nouveaux tags séparés par des virgules")
# Commande "get"
get_parser = subparsers.add_parser("get", help="Afficher les détails d'une tâche")
get_parser.add_argument("id", help="ID de la tâche")
# Commande "delete"
delete_parser = subparsers.add_parser("delete", help="Supprimer une tâche")
delete_parser.add_argument("id", help="ID de la tâche à supprimer")
return parser
def format_task_for_display(task, detailed=False):
"""Formate une tâche pour l'affichage."""
status_colors = {
TaskStatus.TODO: Fore.RED,
TaskStatus.IN_PROGRESS: Fore.YELLOW,
TaskStatus.DONE: Fore.GREEN
}
color = status_colors.get(task.status, Fore.WHITE)
if not detailed:
return f"{color}[{task.status}]{Style.RESET_ALL} {task.title} {Fore.BLUE}(ID: {task.id[:8]}){Style.RESET_ALL}"
# Affichage détaillé
result = [
f"{Fore.CYAN}ID:{Style.RESET_ALL} {task.id}",
f"{Fore.CYAN}Titre:{Style.RESET_ALL} {task.title}",
f"{Fore.CYAN}Statut:{Style.RESET_ALL} {color}{task.status}{Style.RESET_ALL}",
f"{Fore.CYAN}Créée le:{Style.RESET_ALL} {task.created_at.strftime('%d/%m/%Y %H:%M')}",
f"{Fore.CYAN}Mise à jour:{Style.RESET_ALL} {task.updated_at.strftime('%d/%m/%Y %H:%M')}"
]
if task.description:
result.append(f"{Fore.CYAN}Description:{Style.RESET_ALL} {task.description}")
if task.due_date:
result.append(f"{Fore.CYAN}Échéance:{Style.RESET_ALL} {task.due_date.strftime('%d/%m/%Y')}")
if task.tags:
tags_str = ", ".join(f"{Fore.MAGENTA}{tag}{Style.RESET_ALL}" for tag in task.tags)
result.append(f"{Fore.CYAN}Tags:{Style.RESET_ALL} {tags_str}")
return "\n".join(result)
def main():
"""Point d'entrée principal du programme."""
colorama.init()
parser = setup_argparse()
args = parser.parse_args()
if not args.command:
parser.print_help()
return
commands = TodoCommands()
try:
if args.command == "add":
task = commands.add(
title=args.title,
description=args.description or "",
due_date=args.due,
tags=args.tags
)
print(f"{Fore.GREEN}Tâche ajoutée avec succès !{Style.RESET_ALL}")
print(format_task_for_display(task, detailed=True))
elif args.command == "list":
tasks = commands.list(
status=args.status,
tag=args.tag
)
if not tasks:
print(f"{Fore.YELLOW}Aucune tâche ne correspond aux critères.{Style.RESET_ALL}")
return
print(f"{Fore.CYAN}Tâches ({len(tasks)}):{Style.RESET_ALL}")
for i, task in enumerate(tasks, 1):
print(f"{i}. {format_task_for_display(task)}")
elif args.command == "update":
update_data = {}
if args.title:
update_data["title"] = args.title
if args.description is not None: # Autoriser les descriptions vides
update_data["description"] = args.description
if args.status:
update_data["status"] = args.status
if args.due:
update_data["due_date"] = args.due
if args.tags is not None: # Autoriser les listes de tags vides
update_data["tags"] = args.tags
if not update_data:
print(f"{Fore.YELLOW}Aucune modification spécifiée.{Style.RESET_ALL}")
return
task = commands.update(args.id, **update_data)
print(f"{Fore.GREEN}Tâche mise à jour avec succès !{Style.RESET_ALL}")
print(format_task_for_display(task, detailed=True))
elif args.command == "get":
task = commands.get(args.id)
print(format_task_for_display(task, detailed=True))
elif args.command == "delete":
commands.delete(args.id)
print(f"{Fore.GREEN}Tâche supprimée avec succès !{Style.RESET_ALL}")
except CommandError as e:
print(f"{Fore.RED}Erreur : {e}{Style.RESET_ALL}")
sys.exit(1)
if __name__ == "__main__":
main()
Création du fichier d’initialisation
Pour finir, créons le fichier todo/__init__.py
pour rendre notre package utilisable :
# todo/__init__.py
"""
TodoApp - Une application de gestion de tâches en ligne de commande
"""
__version__ = "0.1.0"
Utilisation de l’application
Maintenant que notre application est créée, voici comment l’utiliser :
# Installer l'application en mode développement
pip install -e .
# Ajouter une nouvelle tâche
todo add "Implémenter la fonctionnalité X" --description "Cette tâche consiste à..." --due "31/12/2023" --tags "dev,urgent"
# Lister toutes les tâches
todo list
# Lister les tâches avec un statut spécifique
todo list --status TODO
# Lister les tâches avec un tag spécifique
todo list --tag urgent
# Afficher les détails d'une tâche
todo get abc123 # Remplacer par un ID réel
# Mettre à jour une tâche
todo update abc123 --status IN_PROGRESS
# Marquer une tâche comme terminée
todo update abc123 --status DONE
# Supprimer une tâche
todo delete abc123
Test de l’application
Pour assurer la qualité de notre application, créons un test unitaire simple pour la classe Task
dans tests/test_models.py
:
# tests/test_models.py
import unittest
from datetime import datetime
from todo.models import Task, TaskStatus
class TestTask(unittest.TestCase):
def test_task_creation(self):
"""Test de la création d'une tâche avec des valeurs par défaut."""
task = Task("Test Task")
self.assertEqual(task.title, "Test Task")
self.assertEqual(task.description, "")
self.assertEqual(task.status, TaskStatus.TODO)
self.assertEqual(task.tags, [])
self.assertIsNone(task.due_date)
def test_task_with_all_fields(self):
"""Test de la création d'une tâche avec tous les champs spécifiés."""
due_date = datetime(2023, 12, 31)
task = Task(
title="Test Task",
description="Test Description",
status=TaskStatus.IN_PROGRESS,
due_date=due_date,
tags=["test", "python"]
)
self.assertEqual(task.title, "Test Task")
self.assertEqual(task.description, "Test Description")
self.assertEqual(task.status, TaskStatus.IN_PROGRESS)
self.assertEqual(task.due_date, due_date)
self.assertEqual(task.tags, ["test", "python"])
def test_task_update(self):
"""Test de la mise à jour d'une tâche."""
task = Task("Initial Title")
old_updated_at = task.updated_at
# Petite pause pour garantir que updated_at sera différent
import time
time.sleep(0.001)
task.update(
title="New Title",
description="New Description",
status=TaskStatus.DONE
)
self.assertEqual(task.title, "New Title")
self.assertEqual(task.description, "New Description")
self.assertEqual(task.status, TaskStatus.DONE)
self.assertNotEqual(task.updated_at, old_updated_at)
def test_to_dict_and_from_dict(self):
"""Test de la sérialisation et désérialisation d'une tâche."""
original_task = Task(
title="Serialization Test",
description="Testing serialization",
tags=["test", "serialization"]
)
# Convertir en dict puis recréer la tâche
task_dict = original_task.to_dict()
recreated_task = Task.from_dict(task_dict)
self.assertEqual(recreated_task.id, original_task.id)
self.assertEqual(recreated_task.title, original_task.title)
self.assertEqual(recreated_task.description, original_task.description)
self.assertEqual(recreated_task.status, original_task.status)
self.assertEqual(recreated_task.tags, original_task.tags)
if __name__ == "__main__":
unittest.main()
Pour exécuter les tests :
python -m pytest tests/
Conclusion
Félicitations ! Vous avez créé une application Python complète qui intègre de nombreux concepts importants :
- Structure de projet : Organisation modulaire avec séparation des responsabilités.
- POO : Utilisation de classes pour représenter les données et le comportement.
- Gestion d’exceptions : Capture et traitement appropriés des erreurs.
- Persistance des données : Stockage des tâches au format JSON.
- Interface en ligne de commande : API utilisateur claire et cohérente.
- Tests : Vérification du bon fonctionnement des composants.
Ce projet vous a permis de mettre en pratique les compétences acquises tout au long de cette série de tutoriels. Vous pouvez maintenant l’enrichir en ajoutant de nouvelles fonctionnalités, comme :
- Une interface graphique avec une bibliothèque comme Tkinter ou PyQt
- Une API web avec Flask ou FastAPI
- Une synchronisation avec des services cloud
- Des rappels pour les tâches dont l’échéance approche
- Des statistiques sur les tâches accomplies
N’hésitez pas à explorer ces extensions pour continuer à développer vos compétences en Python.
Cette série de tutoriels touche à sa fin, mais votre voyage avec Python ne fait que commencer. Continuez à pratiquer, à explorer et à créer des projets qui vous passionnent. Bonne programmation !
Commentaires
Les commentaires sont alimentés par GitHub Discussions
Connectez-vous avec GitHub pour participer à la discussion