Centro de mando de formaciones (IV):Implementación de la capa de datos: estructura, repositorios y pruebas
En los posts anteriores definimos los objetivos del proyecto, diseñamos la arquitectura y dimos los primeros pasos de implementación. En este cuarto artículo entramos en un terreno clave: la capa de datos, es decir, todo lo que tiene que ver con almacenar, consultar y mantener la información de nuestro planificador de formaciones.
Implementación de la capa de datos: estructura, repositorios y pruebas
En los posts anteriores definimos los objetivos del proyecto, diseñamos la arquitectura y dimos los primeros pasos de implementación. En este cuarto artículo entramos en un terreno clave: la capa de datos, es decir, todo lo que tiene que ver con almacenar, consultar y mantener la información de nuestro planificador de formaciones.
Diseño físico de la base de datos
Hemos elegido SQLite como motor por su sencillez y su integración nativa con Python. El esquema contempla todas las entidades que definimos en la fase de análisis:
- Cliente → información de las empresas y personas de contacto.
- Tema → catálogo de temáticas formativas.
- FormacionBase → catálogo de formaciones generales (ej. “Excel Básico”).
- ContratacionClienteFormacion → cada curso contratado por un cliente concreto.
- Sesion → sesiones individuales de cada contratación.
- Adjunto → ficheros asociados (contratos, guiones, etc.).
- InteraccionCliente → histórico de interacciones comerciales (mini-CRM).
El esquema completo se encuentra en planificador/data/schema.sql. Un fragmento de ejemplo:
CREATE TABLE IF NOT EXISTS Cliente (
id_cliente INTEGER PRIMARY KEY AUTOINCREMENT,
empresa TEXT NOT NULL,
persona_contacto TEXT,
telefono TEXT,
email TEXT,
direccion TEXT,
cif TEXT,
notas TEXT,
color_hex TEXT DEFAULT '#377eb8',
UNIQUE (cif)
);
CREATE TABLE IF NOT EXISTS InteraccionCliente (
id_interaccion INTEGER PRIMARY KEY AUTOINCREMENT,
id_cliente INTEGER NOT NULL,
fecha TEXT NOT NULL,
tipo TEXT NOT NULL CHECK (tipo IN ('llamada','email','reunion','mensaje','otro')),
descripcion TEXT,
resultado TEXT NOT NULL DEFAULT 'pendiente'
CHECK (resultado IN ('pendiente','negociacion','aceptado','rechazado','sin_respuesta')),
proxima_accion TEXT,
fecha_proxima_accion TEXT,
crear_recordatorio INTEGER NOT NULL DEFAULT 0 CHECK (crear_recordatorio IN (0,1)),
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
FOREIGN KEY (id_cliente) REFERENCES Cliente(id_cliente)
ON UPDATE CASCADE ON DELETE CASCADE
);
Gestor de base de datos (db_manager.py)
Para trabajar con SQLite de manera centralizada, hemos creado un gestor de conexiones:
import sqlite3
from pathlib import Path
DB_PATH = Path(__file__).resolve().parents[2] / "planificador.db"
SCHEMA_PATH = Path(__file__).resolve().parents[1] / "data" / "schema.sql"
def get_connection():
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
conn.execute("PRAGMA foreign_keys = ON;")
return conn
def init_db(force: bool = False):
if force and DB_PATH.exists():
DB_PATH.unlink()
with sqlite3.connect(DB_PATH) as conn:
conn.execute("PRAGMA foreign_keys = ON;")
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
conn.executescript(f.read())
conn.commit()
Este módulo nos permite inicializar la base de datos y obtener conexiones seguras para trabajar con claves foráneas.
Repositorios CRUD
Cada entidad del modelo de datos tiene su propio repositorio en planificador/data/repositories/. Estos repositorios encapsulan las operaciones CRUD (crear, leer, actualizar, borrar), manteniendo la lógica de acceso a datos separada de la lógica de negocio.
Ejemplo: repositorio de Cliente (cliente_repo.py):
from planificador.data.db_manager import get_connection
class ClienteRepository:
@staticmethod
def crear(empresa, cif, persona_contacto=None, telefono=None, email=None,
direccion=None, notas=None, color_hex="#377eb8"):
with get_connection() as conn:
cur = conn.execute("""
INSERT INTO Cliente (empresa, persona_contacto, telefono, email, direccion, cif, notas, color_hex)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
""", (empresa, persona_contacto, telefono, email, direccion, cif, notas, color_hex))
conn.commit()
return cur.lastrowid
@staticmethod
def obtener_por_id(id_cliente):
with get_connection() as conn:
return conn.execute("SELECT * FROM Cliente WHERE id_cliente=?", (id_cliente,)).fetchone()
@staticmethod
def listar():
with get_connection() as conn:
return conn.execute("SELECT * FROM Cliente ORDER BY empresa").fetchall()
@staticmethod
def actualizar(id_cliente, **campos):
if not campos:
return
sets = ", ".join(f"{k}=?" for k in campos)
valores = list(campos.values()) + [id_cliente]
with get_connection() as conn:
conn.execute(f"UPDATE Cliente SET {sets} WHERE id_cliente=?", valores)
conn.commit()
@staticmethod
def borrar(id_cliente):
with get_connection() as conn:
conn.execute("DELETE FROM Cliente WHERE id_cliente=?", (id_cliente,))
conn.commit()
De igual forma hemos implementado repositorios para Tema, FormacionBase, ContratacionClienteFormacion, Sesion, Adjunto e InteraccionCliente.
Pruebas unitarias con Pytest
Para garantizar que todo funciona, hemos creado tests unitarios que validan las operaciones básicas de cada repositorio.
Ejemplo: prueba para ClienteRepository (tests/test_cliente_repo.py):
import pytest
from planificador.data.db_manager import init_db
from planificador.data.repositories.cliente_repo import ClienteRepository
@pytest.fixture(autouse=True)
def setup_db():
init_db(force=True)
def test_crud_cliente():
cliente_id = ClienteRepository.crear("Empresa Demo", "B12345678")
cliente = ClienteRepository.obtener_por_id(cliente_id)
assert cliente["empresa"] == "Empresa Demo"
ClienteRepository.actualizar(cliente_id, empresa="Empresa Modificada")
cliente = ClienteRepository.obtener_por_id(cliente_id)
assert cliente["empresa"] == "Empresa Modificada"
ClienteRepository.borrar(cliente_id)
assert ClienteRepository.obtener_por_id(cliente_id) is None
Prueba de integración
Además, hemos preparado un test de integración que recorre todo el flujo de negocio:
- Crear un cliente.
- Registrar un tema.
- Crear una formación base.
- Contratarla para ese cliente.
- Programar una sesión.
- Registrar una interacción comercial.
- Asociar un adjunto.
El test valida que todos los repositorios funcionan juntos y que la base de datos mantiene la integridad.
Conclusión
Con esta fase hemos completado la capa de datos de nuestro planificador:
- Base de datos SQLite con esquema sólido.
- Gestor de conexiones centralizado.
- Repositorios CRUD para todas las entidades.
- Tests unitarios y de integración que validan la implementación.
El proyecto ahora cuenta con un pilar estable sobre el que construiremos la lógica de negocio y la interfaz de usuario en las siguientes fases.
👉 En el próximo post abordaremos la capa de dominio, definiendo clases orientadas a objetos que representen clientes, formaciones, contrataciones, sesiones e interacciones, y añadiendo la primera lógica de negocio real.