Entrega Incremental 4 - JSON, CSV y Patrón Repository
REQUIERE E3 (Prototipo Web) completada. Se agregará persistencia al prototipo web manteniendo la misma interfaz.
Estas clases preparan los conocimientos necesarios para esta evaluación:
Implementar persistencia real usando archivos JSON y el patrón Repository. Los datos deben sobrevivir al reinicio del servidor sin cambios en la API.
Individual o en parejas (máximo 2 personas). Debe ser el mismo equipo de E1-E3.
Formato de entrega: Repositorio GitHub actualizado con carpeta src/infrastructure/ (Repository JSON), carpeta data/ (ejemplo de datos). README.md debe incluir comando para poblar datos de prueba.
Descripción: Abstraer el almacenamiento detrás de interfaces para desacoplar el dominio de la persistencia.
IUsuarioRepository: Interfaz con métodos guardar(), obtener(), listar(), eliminar()IProyectoRepository: Interfaz similar para proyectosITareaRepository: Interfaz similar para tareasUsuarioRepositoryJSON(IUsuarioRepository): Persistencia en data/usuarios.jsonProyectoRepositoryJSON(IProyectoRepository): Persistencia en data/proyectos.jsonTareaRepositoryJSON(ITareaRepository): Persistencia en data/tareas.jsonDepends()Descripción: Convertir objetos del dominio a/desde JSON correctamente.
to_dict() -> dict: Serializa el objeto a diccionariofrom_dict(data: dict) -> Clase: Método de clase para deserializar{
"usuarios": [
{"id": 1, "username": "juan", "email": "juan@test.com", "activo": true}
],
"proyectos": [
{"id": 1, "nombre": "TaskFlow", "lider_id": 1, "tarea_ids": [1, 2]}
]
}
Descripción: El sistema debe ser robusto ante archivos corruptos o inexistentes.
C:\Users\...Path(__file__).parent / "data" / "usuarios.json"Descripción: Proteger datos sensibles en archivos.
bcrypt o hashlib.sha256data/*.json del repositoriodata/ejemplo_usuarios.json con datos de pruebaimport hashlib
def hashear_password(password: str) -> str:
"""Hashea un password con SHA-256."""
return hashlib.sha256(password.encode()).hexdigest()
def verificar_password(password: str, hash_guardado: str) -> bool:
"""Verifica si el password coincide con el hash."""
return hashear_password(password) == hash_guardado
from abc import ABC, abstractmethod
from pathlib import Path
import json
from typing import Optional, List
from src.domain.usuario import Usuario
class IUsuarioRepository(ABC):
"""Interfaz del repositorio de usuarios."""
@abstractmethod
def guardar(self, usuario: Usuario, usuario_id: int) -> None:
"""Guarda un usuario."""
pass
@abstractmethod
def obtener(self, usuario_id: int) -> Optional[Usuario]:
"""Obtiene un usuario por ID."""
pass
@abstractmethod
def listar(self) -> List[Usuario]:
"""Lista todos los usuarios."""
pass
@abstractmethod
def eliminar(self, usuario_id: int) -> bool:
"""Elimina un usuario. Retorna True si existía."""
pass
class UsuarioRepositoryJSON(IUsuarioRepository):
"""Implementación JSON del repositorio de usuarios."""
def __init__(self, archivo_path: Path = None):
self.archivo_path = archivo_path or Path(__file__).parent.parent / "data" / "usuarios.json"
self._asegurar_archivo_existe()
def _asegurar_archivo_existe(self) -> None:
"""Crea el archivo si no existe."""
if not self.archivo_path.exists():
self.archivo_path.parent.mkdir(parents=True, exist_ok=True)
self._escribir_datos({"usuarios": {}, "next_id": 1})
def _leer_datos(self) -> dict:
"""Lee los datos del archivo JSON."""
try:
with open(self.archivo_path, 'r', encoding='utf-8') as f:
return json.load(f)
except json.JSONDecodeError as e:
# Crear backup del archivo corrupto
backup_path = self.archivo_path.with_suffix('.json.corrupt')
self.archivo_path.rename(backup_path)
self._asegurar_archivo_existe()
raise RuntimeError(f"JSON corrupto, backup creado en {backup_path}") from e
def _escribir_datos(self, datos: dict) -> None:
"""Escribe los datos al archivo JSON."""
with open(self.archivo_path, 'w', encoding='utf-8') as f:
json.dump(datos, f, indent=2, ensure_ascii=False)
def guardar(self, usuario: Usuario, usuario_id: int) -> None:
datos = self._leer_datos()
datos["usuarios"][str(usuario_id)] = usuario.to_dict()
datos["usuarios"][str(usuario_id)]["id"] = usuario_id
self._escribir_datos(datos)
def obtener(self, usuario_id: int) -> Optional[Usuario]:
datos = self._leer_datos()
usuario_data = datos["usuarios"].get(str(usuario_id))
if usuario_data:
return Usuario.from_dict(usuario_data)
return None
def listar(self) -> List[Usuario]:
datos = self._leer_datos()
return [Usuario.from_dict(u) for u in datos["usuarios"].values()]
def eliminar(self, usuario_id: int) -> bool:
datos = self._leer_datos()
if str(usuario_id) in datos["usuarios"]:
del datos["usuarios"][str(usuario_id)]
self._escribir_datos(datos)
return True
return False
Serialización bidireccional en Usuario, Proyecto, Tarea
IUsuarioRepository, IProyectoRepository, ITareaRepository
Lectura/escritura JSON con manejo de errores
def get_usuario_repo(): return UsuarioRepositoryJSON()
Recibir repo vía Depends(), reemplazar dicts en memoria
Crear dato, reiniciar servidor, verificar que persiste
| Criterio | Excelente (5.0) | Aceptable (3.5) | Insuficiente | Peso |
|---|---|---|---|---|
| Funcionalidad | Datos persisten tras reinicio, CRUD completo funciona | Persiste pero algunos CRUD fallan | No persiste o pierde datos | 35% |
| Patrón Repository | Interfaces ABC, inyección Depends(), sin cambios en API | Repository sin interfaz o inyección parcial | Sin Repository o lógica mezclada | 25% |
| Integración E3 | API idéntica, solo cambió la implementación | Cambios menores en endpoints | Rompió la API o la rehizo | 25% |
| Seguridad | Passwords hasheados, .gitignore correcto | Passwords hasheados pero sin .gitignore | Passwords en texto plano | 15% |
El patrón Repository que implementes aquí facilitará migrar a SQL en E5: Base de Datos. Solo cambiarás la implementación, no la interfaz.