المشروع المتكامل
من التصميم إلى التنفيذ
إنشاء مشروع Python متكامل
تهانينا! لقد أنهيتم جميع الدروس السابقة واكتسبتم قاعدة معرفية متينة في Python. حان الآن وقت تطبيق كل ما تعلمتموه عمليًا من خلال تطوير مشروع متكامل من البداية إلى النهاية.
في هذا الدرس، سنبني تطبيقًا لإدارة المهام (todo list) يعتمد على واجهة سطر الأوامر. سيجمع هذا المشروع معظم المفاهيم التي تناولناها في هذه السلسلة من الدروس، ومنها:
- البرمجة كائنية التوجه
- معالجة الاستثناءات
- التعامل مع ملفات JSON
- هياكل التحكم
- المكتبات القياسية في Python
- أفضل ممارسات التطوير
تحضير المشروع
هيكل المشروع
لنبدأ بإنشاء هيكل مشروع نظيف ومنظم:
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
إعداد البيئة الافتراضية
لننشئ بيئة افتراضية لمشروعنا:
# إنشاء البيئة الافتراضية
python -m venv venv
# تفعيل البيئة الافتراضية
# على Windows
venv\Scripts\activate
# على macOS/Linux
source venv/bin/activate
# تثبيت الاعتماديات
pip install -e .
تحديد الاعتماديات
لننشئ ملف requirements.txt يحتوي على الاعتماديات اللازمة:
# requirements.txt
colorama>=0.4.4 # لتلوين النصوص في الطرفية
pytest>=6.2.5 # للاختبارات الوحدوية
وملف setup.py لجعل الحزمة قابلة للتثبيت:
# 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",
],
},
)
تطوير نموذج البيانات
لنبدأ بتعريف فئات النموذج في الملف 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
إدارة التخزين
لننشئ الآن وحدة التخزين لحفظ مهامنا بصيغة 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}")
تنفيذ الأوامر
لننشئ وحدة الأوامر لإدارة العمليات على المهام:
# 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}")
واجهة سطر الأوامر
وأخيرًا، لننشئ واجهة سطر الأوامر في الملف 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:
"""تهيئة محلل الوسائط."""
parser = argparse.ArgumentParser(
description="Application de gestion de tâches en ligne de commande"
)
subparsers = parser.add_subparsers(dest="command", help="Commandes disponibles")
# أمر "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")
# أمر "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")
# أمر "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")
# أمر "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")
# أمر "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):
"""تنسيق مهمة لعرضها."""
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}"
# عرض مفصّل
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():
"""نقطة الدخول الرئيسية للبرنامج."""
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: # السماح بالأوصاف الفارغة
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: # السماح بقوائم الوسوم الفارغة
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()
إنشاء ملف التهيئة
وأخيرًا، لننشئ الملف todo/__init__.py لجعل الحزمة قابلة للاستخدام:
# todo/__init__.py
"""
TodoApp - تطبيق لإدارة المهام عبر سطر الأوامر
"""
__version__ = "0.1.0"
استخدام التطبيق
بعد أن أنشأنا تطبيقنا، إليك طريقة استخدامه:
# تثبيت التطبيق في وضع التطوير
pip install -e .
# إضافة مهمة جديدة
todo add "Implémenter la fonctionnalité X" --description "Cette tâche consiste à..." --due "31/12/2023" --tags "dev,urgent"
# عرض جميع المهام
todo list
# عرض المهام ذات حالة محددة
todo list --status TODO
# عرض المهام ذات وسم محدد
todo list --tag urgent
# عرض تفاصيل مهمة
todo get abc123 # استبدله بمعرّف حقيقي
# تحديث مهمة
todo update abc123 --status IN_PROGRESS
# وضع علامة «منجزة» على مهمة
todo update abc123 --status DONE
# حذف مهمة
todo delete abc123
اختبار التطبيق
لضمان جودة تطبيقنا، لننشئ اختبار وحدة بسيطًا للفئة Task في الملف 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):
"""اختبار إنشاء مهمة بالقيم الافتراضية."""
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):
"""اختبار إنشاء مهمة مع تحديد جميع الحقول."""
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):
"""اختبار تحديث مهمة."""
task = Task("Initial Title")
old_updated_at = task.updated_at
# توقف قصير لضمان اختلاف قيمة updated_at
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):
"""اختبار تسلسل المهمة وإلغاء تسلسلها."""
original_task = Task(
title="Serialization Test",
description="Testing serialization",
tags=["test", "serialization"]
)
# التحويل إلى dict ثم إعادة إنشاء المهمة
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()
لتشغيل الاختبارات:
python -m pytest tests/
الخاتمة
تهانينا! لقد أنشأتم تطبيق Python متكاملًا يجمع عددًا من المفاهيم المهمة:
- بنية المشروع: تنظيم معياري مع فصل المسؤوليات.
- البرمجة كائنية التوجه (POO): استخدام الفئات لتمثيل البيانات والسلوك.
- معالجة الاستثناءات: التقاط الأخطاء ومعالجتها بالشكل المناسب.
- استمرارية البيانات: تخزين المهام بصيغة JSON.
- واجهة سطر الأوامر: واجهة استخدام واضحة ومتسقة.
- الاختبارات: التحقق من حسن عمل المكونات.
أتاح لكم هذا المشروع تطبيق المهارات التي اكتسبتموها على امتداد هذه السلسلة من الدروس. يمكنكم الآن تطويره بإضافة ميزات جديدة، مثل:
- واجهة رسومية باستخدام مكتبة مثل Tkinter أو PyQt
- واجهة API للويب باستخدام Flask أو FastAPI
- مزامنة مع خدمات السحابة
- تذكيرات بالمهام التي يقترب موعد استحقاقها
- إحصائيات حول المهام المنجزة
لا تترددوا في استكشاف هذه الإضافات لمواصلة تنمية مهاراتكم في Python.
تقترب هذه السلسلة من الدروس من نهايتها، لكن رحلتكم مع Python لا تزال في بدايتها. واصلوا التدرب والاستكشاف وإنشاء المشاريع التي تشغفكم. برمجة موفقة!
التعليقات
التعليقات مدعومة عبر GitHub Discussions
سجّل الدخول باستخدام GitHub للمشاركة في النقاش