Saltar al contenido

# conceptos-avanzados-en-python

Conceptos Avanzados en Python

Volver a la portada del curso
Notebook

Desarrollo 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álogo PRODUCTOS, la excepción FilaInvalidaError(ValueError) y el Pedido como @dataclass(frozen=True, slots=True) con importe calculado y el constructor alternativo desde_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 generador leer_pedidos, que produce pedidos válidos y acumula los errores en una lista.
  • reporte.py: la dataclass Reporte y construir_reporte, que usa Counter y defaultdict.
  • exportar.py: ExportadorTexto y ExportadorJSON, intercambiables porque cumplen el mismo Protocol.
  • cli.py: el argparse y la función main(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, usa max(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.DictReader consume el encabezado, así que la primera fila de datos es la línea 2: usa enumerate(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.exit dentro de main. Devuelve el código y deja que el punto de entrada salga; así puedes probar main sin cerrar el intérprete.
  • Usar print para los avisos. El conteo de filas descartadas va con logging a 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.

Recursos