# conceptos-avanzados-en-python
Conceptos Avanzados en Python
Volver a la portada del cursoDesarrollo de la Aplicación Completa
Sección 4 · Proyecto Final de Python Intermedio
Desarrollo de la aplicación completa
En esta práctica juntas todo lo del curso en una sola herramienta: ventas, un programa de línea de comandos que lee el CSV de pedidos de PyShop, descarta las filas dañadas sin detenerse y muestra un reporte por producto, ciudad y cliente. El notebook adjunto, aplicacion-ventas, la construye módulo por módulo con el mismo código que el paquete instalable del proyecto final. Ejecuta las celdas en orden y compara cada salida con las cifras de esta guía.
Un módulo por sección
La aplicación se divide en seis piezas. Cada una tiene una sola responsabilidad y depende solo de las anteriores, así que puedes probarlas por separado:
modelos.py: el catálogoPRODUCTOS, la excepciónFilaInvalidaError(ValueError)y elPedidocomo@dataclass(frozen=True, slots=True)conimportecalculado y el constructor alternativodesde_fila.datos.py:generar_csv(n, semilla), que produce siempre los mismos 1000 pedidos con unas cuantas filas dañadas a propósito.lectura.py: el generadorleer_pedidos, que produce pedidos válidos y acumula los errores en una lista.reporte.py: la dataclassReporteyconstruir_reporte, que usaCounterydefaultdict.exportar.py:ExportadorTextoyExportadorJSON, intercambiables porque cumplen el mismoProtocol.cli.py: elargparsey la funciónmain(argv).
Separar el cálculo (Reporte) de la presentación (los exportadores) es la decisión que más simplifica el resto: agregar un formato nuevo no toca la lectura ni el cálculo.
Probar la línea de comandos sin terminal
La clave para trabajar la CLI en un notebook es que main recibe la lista de argumentos. Cuando el programa corre instalado, argv=None hace que argparse lea sys.argv; en el notebook y en las pruebas le pasas la lista a mano y revisas el código de salida que devuelve:
import tempfile
from contextlib import chdir
with tempfile.TemporaryDirectory() as carpeta, chdir(carpeta):
main(["--demo", "pedidos.csv"])
codigo = main(["pedidos.csv", "--top", "3"])
print("código de salida:", codigo)
La carpeta temporal se borra al salir del with, y contextlib.chdir (Python 3.11+) regresa a la carpeta original. Es el mismo patrón de context manager que viste en la sección de errores.
Cifras que debes obtener
Con generar_csv(1000, 7), el reporte completo debe mostrar:
- 981 pedidos válidos y 19 filas con error: 9 con cantidad no numérica, 6 con cantidad no positiva y 4 con producto desconocido. El primer error es
línea 111: cantidad no positiva -1. - Venta total de $718,890.00.
- Productos por piezas: Taza 875, Libreta 836, Ebook de Python 564, Mouse 366 y Teclado 242. El Teclado vende menos piezas, pero es el que más factura: $217,558.00.
- Ciudades por venta: CDMX $158,538.00, Lima $157,843.50, Bogotá $149,033.00, Monterrey $134,706.00 y Santiago $118,769.50.
- Mejor cliente: C187, con $11,966.00 en 9 pedidos.
- Pedido más caro: el #980, 5 teclados por $4,495.00 en Monterrey. Hay 10 pedidos empatados con ese importe y
max(pedidos)devuelve el último de ellos: con@total_ordering, dos pedidos empatados no son iguales para el__eq__de la dataclass. Si necesitas el primero, usamax(pedidos, key=lambda p: p.importe)(id 52).
Además, ventas no-existe.csv devuelve el código 1 y, sin argumentos, argparse termina con el código 2.
Pruebas con assert
La sección de pruebas convierte esas cifras en afirmaciones. Si cambias algo y una cifra se mueve, la celda se detiene con AssertionError y sabes exactamente qué se rompió:
assert rep.validos == 981 and rep.num_errores == 19
assert rep.total == 718890.0
assert rep.mejor_cliente == ("C187", 11966.0, 9)
assert main(["no-existe.csv"]) == 1
En el paquete, las mismas comprobaciones viven en tests/test_ventas.py y se ejecutan con pytest -q.
Trampas comunes
- Numerar mal las líneas.
csv.DictReaderconsume el encabezado, así que la primera fila de datos es la línea 2: usaenumerate(filas, start=2). - Sumar flotantes sin redondear. La suma de 981 importes acumula error de coma flotante; redondea a dos decimales al final, no en cada paso.
- Llamar a
sys.exitdentro demain. Devuelve el código y deja que el punto de entrada salga; así puedes probarmainsin cerrar el intérprete. - Usar
printpara los avisos. El conteo de filas descartadas va conlogginga la salida de error, para no ensuciar el JSON cuando se redirige la salida.
Ejercicio
Al final del notebook agregas la venta por día. El resultado esperado es el 6 de septiembre de 2026, con $32,936.00. Después intenta llevarlo al paquete: un campo nuevo en Reporte y una línea más en cada exportador, sin tocar lectura.py.