# sql-desde-python
SQL desde Python
Volver a la portada del cursoDesarrollo de Aplicación Final
Sección 6 · Proyecto Práctico Final
Proyecto final: aplicación de pedidos e inventario de PyShop
Durante el curso consultaste la base de PyShop, cambiaste datos con transacciones, manejaste errores y aceleraste consultas con índices. Ahora vas a juntar todo en una aplicación pequeña pero completa: un módulo de acceso a datos y un programa de consola para registrar clientes, productos y pedidos, controlar el inventario y generar reportes. Se usan Python 3.12 o superior y el módulo estándar sqlite3; pandas y SQLAlchemy solo para los extras. El notebook proyecto-app-pedidos contiene una solución de referencia: consúltalo al terminar, no antes.
Contexto
El equipo de PyShop registra los pedidos a mano y descubre tarde que vendió productos sin existencias. Quiere una herramienta que nunca deje un pedido a medias, que avise con claridad cuando algo no se puede hacer y que responda en segundos las preguntas de siempre: cuánto se vende por categoría, ciudad y mes, qué productos se agotan y qué clientes no han comprado.
Datos
Usa la función crear_bd de la primera celda de cualquier notebook del curso, con otra semilla y en un archivo:
con = crear_bd("pyshop.db", semilla=2026)
for t in ("clientes", "productos",
"pedidos", "detalle"):
sql = f"SELECT COUNT(*) FROM {t}"
print(t, con.execute(sql).fetchone()[0])
con.close()
# clientes 300, productos 40,
# pedidos 2000, detalle 3489
Si tu tabla detalle no tiene 3489 filas, revisa la semilla. Todas las demás cifras debes obtenerlas tú.
Requisitos mínimos
- Módulo
pyshop_db.py. Una funciónconectar(ruta)que abra la base conPRAGMA foreign_keys = ONysqlite3.Row, y una excepción propiaErrorTienda(con al menos una subclase, comoStockInsuficiente). - Altas.
registrar_clienteyregistrar_producto, con parámetros y transacción. Un correo o nombre repetido, un precio no positivo o un dato faltante deben terminar enErrorTiendacon un mensaje claro. - Pedidos.
registrar_pedido(con, cliente_id, items), conitemscomo lista de(producto_id, cantidad): inserta el pedido y su detalle con el precio del momento y descuenta el stock, todo en una transacción. Si un artículo no alcanza, el producto o el cliente no existe, o la lista está vacía, la base no debe cambiar. - Reportes, sin contar los pedidos cancelados en el ingreso:
- R1. Ingreso y número de pedidos por categoría.
- R2. Ingreso por ciudad.
- R3. Ingreso y pedidos por mes.
- R4. Los cinco productos con más unidades vendidas.
- R5. Clientes sin pedidos (con
LEFT JOIN). - R6. Productos con stock menor que un límite que recibe la función.
- Índices. Crea al menos dos y justifica cada uno con la salida de
EXPLAIN QUERY PLANantes (SCAN) y después (SEARCH). Explica por qué no indexaste alguna otra columna. - Programa de consola
app.pyconargparsey subcomandos:reporte,pedidoydevolver(o un menú coninput, si lo prefieres). Ante unErrorTiendamuestra el mensaje y termina con código 1, sin rastreo. - Pruebas en
test_pyshop_db.py: al menos seis funciones conassertsobre bases:memory:, incluidas un pedido válido que descuenta stock, un pedido imposible que no deja rastro (pedidos, detalle y stock iguales) y un correo repetido.
Pistas de implementación
Con RETURNING obtienes el id nuevo en la misma sentencia:
sql = ("INSERT INTO pedidos (cliente_id, fecha, estado) "
"VALUES (?, ?, 'pagado') RETURNING id")
pid = con.execute(sql, (cliente, fecha)).fetchone()[0]
Valida el stock antes de escribir para dar un buen mensaje, pero deja que el CHECK (stock >= 0) siga como red de seguridad. Traduce solo los errores que entiendes:
try:
with con:
...
except sqlite3.IntegrityError as e:
if "FOREIGN KEY" in str(e):
msg = "No existe el cliente."
raise ErrorTienda(msg) from e
raise
Para los pedidos de un mes usa un rango sobre fecha (fecha >= ? AND fecha < ?) y no substr, o el índice no servirá. Para un subcomando como pedido 5 --item 3:1 --item 7:2, add_argument("--item", action="append") junta todos los artículos en una lista.
Los datos son sintéticos: si un mes o una ciudad vende más, descríbelo, pero no le inventes una causa.
Extras opcionales
- Devoluciones.
devolver_pedido(con, pedido_id)marca el pedido como cancelado y regresa al stock sus cantidades, todo o nada; rechaza un pedido inexistente o ya cancelado. - Exportar a CSV. Con
pd.read_sql_queryyto_csv(index=False), guarda R1, R2 y R3 en archivos. - SQLAlchemy. Repite un reporte con
create_engine("sqlite:///pyshop.db")ytext()con parámetros:nombre. - Escritura en lote. Importa 100 clientes nuevos con
executemanyen una transacción y mide contra uncommitpor fila.
Entregables
pyshop_db.py: conexión, excepciones, altas, pedidos, reportes e índices.app.py: el programa de consola.test_pyshop_db.py: las pruebas, que deben pasar conpython -m pytesto ejecutando el archivo.INFORME.md(una página): ejemplos de uso de la consola con su salida, los planes antes y después de cada índice y tres conclusiones con cifras de los reportes.
Rúbrica (100 puntos, más 10 de extras)
- Conexión y estructura (10 puntos):
conectarúnico, SQL separado de la lógica, conexiones cerradas. - Altas y errores (15 puntos): parámetros siempre, excepciones específicas y mensajes útiles; ningún
exceptvacío. - Pedidos con transacción (25 puntos): todo o nada comprobado, stock correcto, precio del momento.
- Reportes (20 puntos): seis consultas correctas, sin cancelados en el ingreso.
- Índices (10 puntos): al menos dos, justificados con el plan.
- Consola (10 puntos): subcomandos claros y errores sin rastreo.
- Pruebas (10 puntos): seis o más, aisladas en
:memory:, incluido el caso imposible.