Projeto completo
Da concepção à implementação
Criar um projeto Python completo
Parabéns! Você percorreu todos os tutoriais anteriores e adquiriu uma base sólida de conhecimentos em Python. Agora é hora de colocar em prática tudo o que aprendeu desenvolvendo um projeto completo, de ponta a ponta.
Neste tutorial, vamos construir uma aplicação de gerenciamento de tarefas (todo list) que usará uma interface de linha de comando. Este projeto integrará a maioria dos conceitos que abordamos nesta série de tutoriais, especialmente:
- A programação orientada a objetos
- O tratamento de exceções
- A manipulação de arquivos JSON
- As estruturas de controle
- As bibliotecas padrão do Python
- As boas práticas de desenvolvimento
Preparação do projeto
Estrutura do projeto
Vamos começar criando uma estrutura de projeto limpa e organizada:
todo_app/
│
├── todo/
│ ├── __init__.py
│ ├── models.py # Classes de dados (Task, TaskList)
│ ├── storage.py # Gerenciamento do armazenamento (JSON)
│ ├── commands.py # Implementação dos comandos
│ └── cli.py # Interface de linha de comando
│
├── tests/ # Testes unitários
│ ├── __init__.py
│ ├── test_models.py
│ └── test_storage.py
│
├── data/ # Pasta para armazenar os dados
│ └── tasks.json
│
├── requirements.txt # Dependências do projeto
├── setup.py # Configuração de instalação
└── README.md # Documentação do projeto
Configuração do ambiente virtual
Vamos criar um ambiente virtual para o nosso projeto:
# Criação do ambiente virtual
python -m venv venv
# Ativação do ambiente virtual
# No Windows
venv\Scripts\activate
# No macOS/Linux
source venv/bin/activate
# Instalação das dependências
pip install -e .
Definição das dependências
Vamos criar um arquivo requirements.txt com as dependências necessárias:
# requirements.txt
colorama>=0.4.4 # Para colorir a saída no terminal
pytest>=6.2.5 # Para os testes unitários
E um arquivo setup.py para tornar nosso pacote instalável:
# 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",
],
},
)
Desenvolvimento do modelo de dados
Vamos começar definindo nossas classes de modelo em 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 representando os status possíveis de uma tarefa."""
TODO = auto()
IN_PROGRESS = auto()
DONE = auto()
def __str__(self) -> str:
return self.name.replace("_", " ").title()
class Task:
"""Classe representando uma tarefa."""
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:
"""Atualiza os atributos da tarefa."""
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]:
"""Converte a tarefa em dicionário para serialização."""
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':
"""Cria uma instância de Task a partir de um dicionário."""
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 representando uma lista de tarefas."""
def __init__(self):
self.tasks: Dict[str, Task] = {}
def add_task(self, task: Task) -> None:
"""Adiciona uma tarefa à lista."""
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:
"""Recupera uma tarefa pelo seu 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:
"""Atualiza uma tarefa existente."""
task = self.get_task(task_id)
task.update(**kwargs)
return task
def delete_task(self, task_id: str) -> None:
"""Remove uma tarefa da lista."""
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]:
"""Filtra as tarefas por status e/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]:
"""Converte a lista de tarefas em dicionário para serialização."""
return {
"tasks": [task.to_dict() for task in self.tasks.values()]
}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'TaskList':
"""Cria uma instância de TaskList a partir de um dicionário."""
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
Gerenciamento do armazenamento
Agora, vamos criar o módulo de armazenamento para persistir nossas tarefas em JSON:
# todo/storage.py
import json
import os
from pathlib import Path
from typing import Optional
from .models import TaskList
class StorageError(Exception):
"""Exceção lançada em caso de erros de armazenamento."""
pass
class Storage:
"""Classe que gerencia o armazenamento das tarefas em JSON."""
def __init__(self, file_path: Optional[str] = None):
if file_path:
self.file_path = Path(file_path)
else:
# Caminho padrão
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:
"""Salva a lista de tarefas no arquivo 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:
"""Carrega a lista de tarefas a partir do arquivo 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}")
Implementação dos comandos
Vamos criar o módulo de comandos para gerenciar as operações sobre as tarefas:
# 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):
"""Exceção lançada em caso de erros de comando."""
pass
class TodoCommands:
"""Classe que implementa os comandos da aplicação."""
def __init__(self, storage: Optional[Storage] = None):
self.storage = storage or Storage()
try:
self.task_list = self.storage.load()
except StorageError as e:
# Em caso de erro, começamos com uma lista vazia
print(f"Attention : {e}")
self.task_list = TaskList()
def _save(self) -> None:
"""Salva o estado atual das tarefas."""
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:
"""Adiciona uma nova tarefa."""
# Validação das entradas
if not title:
raise CommandError("Le titre ne peut pas être vide")
# Conversão da data de vencimento
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")
# Tratamento das tags
tag_list = []
if tags:
tag_list = [tag.strip() for tag in tags.split(',')]
# Criação e adição da tarefa
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]:
"""Lista as tarefas com filtragem opcional."""
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:
"""Atualiza uma tarefa existente."""
# Tratamento especial para alguns campos
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:
"""Remove uma tarefa."""
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:
"""Recupera os detalhes de uma tarefa específica."""
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 de linha de comando
Por fim, vamos criar a interface de linha de comando em 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:
"""Configura o parser de argumentos."""
parser = argparse.ArgumentParser(
description="Application de gestion de tâches en ligne de commande"
)
subparsers = parser.add_subparsers(dest="command", help="Commandes disponibles")
# Comando "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")
# Comando "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")
# Comando "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")
# Comando "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")
# Comando "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):
"""Formata uma tarefa para exibição."""
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}"
# Exibição detalhada
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():
"""Ponto de entrada principal do programa."""
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: # Permitir descrições vazias
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: # Permitir listas de tags vazias
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()
Criação do arquivo de inicialização
Para finalizar, vamos criar o arquivo todo/__init__.py para tornar nosso pacote utilizável:
# todo/__init__.py
"""
TodoApp - Um aplicativo de gerenciamento de tarefas em linha de comando
"""
__version__ = "0.1.0"
Utilização do aplicativo
Agora que nosso aplicativo está criado, veja como utilizá-lo:
# Instalar o aplicativo em modo de desenvolvimento
pip install -e .
# Adicionar uma nova tarefa
todo add "Implémenter la fonctionnalité X" --description "Cette tâche consiste à..." --due "31/12/2023" --tags "dev,urgent"
# Listar todas as tarefas
todo list
# Listar as tarefas com um status específico
todo list --status TODO
# Listar as tarefas com uma tag específica
todo list --tag urgent
# Exibir os detalhes de uma tarefa
todo get abc123 # Substitua por um ID real
# Atualizar uma tarefa
todo update abc123 --status IN_PROGRESS
# Marcar uma tarefa como concluída
todo update abc123 --status DONE
# Excluir uma tarefa
todo delete abc123
Teste do aplicativo
Para garantir a qualidade do nosso aplicativo, vamos criar um teste unitário simples para a classe Task em 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):
"""Teste da criação de uma tarefa com valores padrão."""
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):
"""Teste da criação de uma tarefa com todos os campos especificados."""
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):
"""Teste da atualização de uma tarefa."""
task = Task("Initial Title")
old_updated_at = task.updated_at
# Pequena pausa para garantir que updated_at será diferente
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):
"""Teste da serialização e desserialização de uma tarefa."""
original_task = Task(
title="Serialization Test",
description="Testing serialization",
tags=["test", "serialization"]
)
# Converter para dict e depois recriar a tarefa
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()
Para executar os testes:
python -m pytest tests/
Conclusão
Parabéns! Você criou uma aplicação Python completa que reúne muitos conceitos importantes:
- Estrutura de projeto: organização modular com separação de responsabilidades.
- POO: uso de classes para representar os dados e o comportamento.
- Tratamento de exceções: captura e tratamento adequados dos erros.
- Persistência de dados: armazenamento das tarefas no formato JSON.
- Interface de linha de comando: API de usuário clara e consistente.
- Testes: verificação do bom funcionamento dos componentes.
Este projeto permitiu que você colocasse em prática as habilidades adquiridas ao longo desta série de tutoriais. Agora você pode enriquecê-lo adicionando novas funcionalidades, como:
- Uma interface gráfica com uma biblioteca como Tkinter ou PyQt
- Uma API web com Flask ou FastAPI
- Uma sincronização com serviços de nuvem
- Lembretes para as tarefas com prazo se aproximando
- Estatísticas sobre as tarefas concluídas
Não hesite em explorar essas extensões para continuar desenvolvendo suas habilidades em Python.
Esta série de tutoriais está chegando ao fim, mas sua jornada com Python está apenas começando. Continue praticando, explorando e criando projetos que despertem sua paixão. Boa programação!
Comentários
Os comentários são alimentados pelo GitHub Discussions
Conecte-se com o GitHub para participar da discussão