Evaluación 4

Persistencia en Archivos

Entrega Incremental 4 - JSON, CSV y Patrón Repository

15%
Software
Fecha límite: 23 de abril de 2026
Requisitos Previos

REQUIERE E3 (Prototipo Web) completada. Se agregará persistencia al prototipo web manteniendo la misma interfaz.

Clases Relacionadas

Estas clases preparan los conocimientos necesarios para esta evaluación:

Objetivo de Aprendizaje

Implementar persistencia real usando archivos JSON y el patrón Repository. Los datos deben sobrevivir al reinicio del servidor sin cambios en la API.

Modalidad de Trabajo

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.

Requisitos Técnicos Detallados

1. Patrón Repository

Descripción: Abstraer el almacenamiento detrás de interfaces para desacoplar el dominio de la persistencia.

Interfaces obligatorias (ABC):
Implementaciones JSON:
Inyección de dependencias:
  • FastAPI debe recibir el repositorio vía Depends()
  • Cambiar de JSON a SQL en E5 NO debe modificar los endpoints
2. Serialización JSON

Descripción: Convertir objetos del dominio a/desde JSON correctamente.

Métodos obligatorios en cada clase del dominio:
Manejo de relaciones:
Ejemplo de estructura JSON:
{
  "usuarios": [
    {"id": 1, "username": "juan", "email": "juan@test.com", "activo": true}
  ],
  "proyectos": [
    {"id": 1, "nombre": "TaskFlow", "lider_id": 1, "tarea_ids": [1, 2]}
  ]
}
3. Manejo de Errores

Descripción: El sistema debe ser robusto ante archivos corruptos o inexistentes.

Casos a manejar:
Uso de pathlib:
4. Seguridad

Descripción: Proteger datos sensibles en archivos.

Requisitos obligatorios:
Hash de passwords:
import 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

Ejemplo de Implementación

src/infrastructure/repository.py
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

Estructura de Entrega

Organización de Archivos Obligatoria
src/ ├── domain/ # Dominio con to_dict/from_dict añadidos │ ├── usuario.py │ ├── proyecto.py │ └── tarea.py │ └── infrastructure/ # Nueva capa de persistencia ├── __init__.py ├── repository.py # Interfaces y implementaciones └── serializers.py # Lógica de serialización api/ ├── main.py # Inyección de repositorios └── dependencies.py # Funciones Depends() data/ # Archivos de datos (en .gitignore) ├── .gitkeep ├── usuarios.json ├── proyectos.json ├── tareas.json └── ejemplo_usuarios.json # Datos de prueba para el repo .gitignore # Debe excluir data/*.json requirements.txt # + bcrypt o hashlib README.md # Cómo poblar datos de prueba

Pasos para Completar la Evaluación

1Añadir to_dict/from_dict a clases

Serialización bidireccional en Usuario, Proyecto, Tarea

2Crear interfaces Repository (ABC)

IUsuarioRepository, IProyectoRepository, ITareaRepository

3Implementar RepositoryJSON

Lectura/escritura JSON con manejo de errores

4Configurar dependencias FastAPI

def get_usuario_repo(): return UsuarioRepositoryJSON()

5Actualizar endpoints

Recibir repo vía Depends(), reemplazar dicts en memoria

6Probar persistencia

Crear dato, reiniciar servidor, verificar que persiste

Rúbrica de Evaluación (100%)

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%
Penalizaciones Severas
Conexión con E5

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.

← E3: Web E5: SQL →