# conceptos-avanzados-en-python
Conceptos Avanzados en Python
Volver a la portada del cursoEntrega del Proyecto Final
Sección 4 · Proyecto Final de Python Intermedio
Proyecto final: la herramienta ventas
En este proyecto construyes ventas, una herramienta de línea de comandos que lee un CSV de pedidos de PyShop, descarta las filas dañadas sin detenerse y muestra un reporte de ventas. Debe ser un paquete instalable: tras pip install -e . o uv tool install ., el comando ventas pedidos.csv funciona desde cualquier carpeta. El programa usa solo la biblioteca estándar de Python 3.11+; las pruebas pueden usar pytest. El archivo ventas-cli.zip contiene una solución de referencia completa: consúltalo al terminar, no antes.
Datos de entrada
Columnas: id,fecha,cliente,ciudad,producto,cantidad. Genera los datos con esta función, que siempre produce los mismos 1000 pedidos, con un 2 % de filas dañadas a propósito:
import csv
import io
import random
from datetime import date, timedelta
PRODUCTOS = {"Taza": 149.0, "Teclado": 899.0,
"Ebook de Python": 299.0, "Mouse": 349.5,
"Libreta": 89.0}
CIUDADES = ["CDMX", "Bogotá", "Lima", "Santiago", "Monterrey"]
PESOS = [30, 8, 20, 12, 30]
def generar_csv(n=1000, semilla=7):
rng = random.Random(semilla)
salida = io.StringIO()
w = csv.writer(salida, lineterminator="\n")
w.writerow(["id", "fecha", "cliente", "ciudad",
"producto", "cantidad"])
inicio = date(2026, 9, 1)
for i in range(1, n + 1):
fecha = inicio + timedelta(days=rng.randrange(30))
cliente = f"C{rng.randrange(1, 201):03d}"
ciudad = rng.choice(CIUDADES)
producto = rng.choices(list(PRODUCTOS),
weights=PESOS)[0]
cantidad = str(rng.randint(1, 5))
r = rng.random()
if r < 0.008:
cantidad = "dos"
elif r < 0.014:
producto = "Gorra"
elif r < 0.02:
cantidad = "-1"
w.writerow([i, fecha.isoformat(), cliente, ciudad,
producto, cantidad])
return salida.getvalue()
Cambiar el orden de las llamadas a rng produce otras cifras.
Requisitos mínimos
- Estructura de paquete. Un
pyproject.tomlconrequires-python = ">=3.11"y la entrada[project.scripts]que crea el comandoventas. Código ensrc/ventas/, al menos enmodelos.py,lectura.py,reporte.pyycli.py. - Modelo inmutable.
Pedidoes una@dataclass(frozen=True, slots=True)conimportecomo@property. Se ordena por importe con__lt__y@total_ordering. - Validación con excepción propia. Un
@classmethoddesde_fila(fila, num_linea)que lanzaFilaInvalidaError, subclase deValueErrorcon los atributosnum_lineaymotivo: cantidad no numérica (conraise ... from None), cantidad menor o igual a cero o producto fuera del catálogo. - Lectura tolerante. Un generador
leer_pedidos(texto, errores)que produce pedidos válidos y acumula los errores en la lista sin detenerse. - Reporte. Errores por motivo, venta total, piezas y venta por producto, venta por ciudad (de mayor a menor), mejor cliente y pedido más caro.
- Dos formatos. Texto y JSON como clases intercambiables con el mismo método, sin cadenas de
if. - Interfaz con argparse.
ventas ARCHIVO.csv [--top N] [--formato texto|json] [--salida RUTA]y una opción para escribir el CSV de ejemplo, por ejemploventas --demo pedidos.csv.main(argv=None)recibe la lista de argumentos y devuelve el código de salida. - Errores y avisos. Archivo inexistente: mensaje en la salida de error y código 1, sin traza. Las filas descartadas se informan con
logging(WARNING).
Cifras esperadas sobre generar_csv(1000, 7)
Tu herramienta debe reproducir exactamente estos valores:
- 1001 líneas con el encabezado; primera fila:
1,2026-09-11,C039,Santiago,Mouse,1. - 981 pedidos válidos y 19 errores: 9 por cantidad no numérica, 6 por cantidad no positiva y 4 por producto desconocido. El primero es
línea 111: cantidad no positiva -1. - Venta total: 718890.0.
- Piezas: Taza 875, Libreta 836, Ebook de Python 564, Mouse 366 y Teclado 242.
- Venta por producto: Teclado 217558.0, Ebook de Python 168636.0, Taza 130375.0, Mouse 127917.0 y Libreta 74404.0.
- Venta por ciudad: CDMX 158538.0, Lima 157843.5, Bogotá 149033.0, Monterrey 134706.0 y Santiago 118769.5.
- 200 clientes distintos; el mejor es C187, con 11966.0 en 9 pedidos.
- Pedido más caro: 4495.0 (5 teclados); entre 10 empates,
max(pedidos)devuelve el último, el id 980. ventas no-existe.csvtermina con código 1;ventassin argumentos, con código 2.
Una prueba mínima con pytest:
from ventas.cli import main
def test_reporte_json(tmp_path):
csv_path = tmp_path / "pedidos.csv"
salida = tmp_path / "reporte.json"
main(["--demo", str(csv_path)])
codigo = main([str(csv_path), "--formato", "json",
"--salida", str(salida)])
assert codigo == 0
assert "718890.0" in salida.read_text(encoding="utf-8")
Extras opcionales
- Venta por día y mejor día (resultado esperado: 2026-09-06, 34 pedidos, 32936.0).
- Un exportador CSV o Markdown sin modificar
reporte.py. - Filtros por ciudad o por fechas.
- Dinero con
decimal.Decimalen lugar defloat. - Medir con
cProfilela lectura de 100 000 pedidos y documentar qué optimizaste. - Revisar el código con
ruffy completar los type hints.
Entregables
- El paquete en un zip o un repositorio:
pyproject.toml,src/ventas/,tests/y unREADME.mdcon instrucciones de instalación y uso. - Las pruebas con las cifras esperadas, ejecutables con
pytest -q. - Una captura de terminal (texto copiado): instalación,
ventas --demo pedidos.csv, reporte en texto, reporte JSON con--top 3ypytest. - Una nota breve (media página) que explique dos decisiones de diseño: por qué
Pedidoes inmutable y por qué el cálculo del reporte está separado de los exportadores.
Rúbrica (100 puntos)
- Empaquetado (15 puntos):
pyproject.toml, estructurasrc/y comando instalable. - Modelo y validación (20 puntos): dataclass inmutable,
importecalculado, orden yFilaInvalidaError. - Lectura y reporte (20 puntos): generador tolerante,
Counter/defaultdicty cifras correctas. - Exportadores e interfaz (15 puntos): formatos intercambiables,
argparsey códigos de salida. - Manejo de errores (10 puntos): sin trazas, avisos con
logging, ningúnexcept:desnudo. - Pruebas (10 puntos): las cifras esperadas y los casos de error pasan con
pytest. - Claridad (10 puntos): nombres claros, docstrings, README y nota de diseño.
Los extras no suman puntos, pero consolidan lo aprendido.