Módulo 7: Shipping The Package

Presentación del módulo: el paquete que se entrega

Por qué existe este módulo

Repasa, en números concretos, dónde quedó kiosko_pipeline al cerrar el módulo 6. El módulo 1 lo convirtió de un script suelto (kiosko_report.py) a un paquete instalable con uv, con pyproject.toml y un comando de consola ya cableado (kiosko-pipeline = "kiosko_pipeline:main"). El módulo 2 le agregó diecinueve tests. El módulo 3 reemplazó cada print() por eventos JSON estructurados. El módulo 4 le dio la capacidad de reintentar una falla transitoria y de poner en cuarentena una partición con un dato malo, en vez de tumbar la semana entera — con cinco tests deterministas más. El módulo 5 construyó transform_duckdb.py, una segunda implementación completa de fact_orders, calculada con SQL sobre Parquet — cinco tests más. El módulo 6 construyó transform_polars.py, una tercera implementación, con la API de LazyFrame de Polars — seis tests más. Corre la suite completa tal como la dejó el módulo 6:

uv run pytest -v

Qué esperar (las últimas líneas; treinta y cinco tests en total):

tests/test_transform_polars.py::test_find_unregistered_stores_polars_catches_what_the_inner_join_hid PASSED [ 97%]
tests/test_transform_polars.py::test_find_unregistered_stores_polars_is_empty_for_the_known_week PASSED [100%]

============================== 35 passed in 0.72s ==============================

Treinta y cinco tests en verde, tres motores de transformación probados y de acuerdo hasta el último centavo, logging estructurado, reintentos, cuarentena. Suena a que el trabajo está terminado. No lo está, y el módulo 6 fue explícito sobre por qué al cerrar: "pipeline.py sigue usando transform.py, sin ninguna conexión a transform_duckdb.py ni a transform_polars.py — este módulo construyó una tercera alternativa completa y probada, no reemplazó nada en producción. Esa decisión, explícita y justificada entre las tres, es del módulo 7." Esa frase, literal, es la razón de ser de este módulo.

Conexión con el módulo. Quedan tres huecos concretos, no vagos, entre "un paquete testeado con tres motores probados" y "un paquete que un equipo aceptaría en producción". Primero: no existe ninguna forma de correr kiosko_pipeline sobre un día específicouv run python -m kiosko_pipeline siempre procesa la misma semana fija, WEEK_DAYS, escrita como constante dentro de pipeline.py; un pipeline de producción real necesita que alguien (una persona, o un scheduler) le diga qué partición procesar hoy, sin editar código para cambiarlo. Segundo: la carpeta de datos ("data") y la ruta del warehouse ("data/warehouse.db") están escritas, literales, dentro del código — mover el proyecto a otra máquina, con otra estructura de carpetas, exige editar pipeline.py a mano. Tercero, y el más importante: nadie decidió todavía cuál de los tres motores de transformación —el for con diccionarios, el INNER JOIN de DuckDB, el join de Polars— corre quien de verdad procesa cada corrida real. Este módulo cierra los tres huecos, en ese orden: un entry point de línea de comandos con argparse (lección 2), configuración sin valores hardcodeados (lección 3), y la decisión de motor, cableada de punta a punta junto con logging y reintentos (lección 4) — y cierra con una prueba automatizada de la idempotencia del pipeline completo, con assert, no a ojo (lecciones 5 y 6).

El caso que nos acompaña: la misma semana, una nueva forma de pedirle trabajo

Kiosko no cambia en este módulo — ni un valor, ni una fila. Sigues trabajando con la misma semana fija (orders_2026-08-03.csv a orders_2026-08-09.csv, cuarenta órdenes, 106.15 de revenue total, S01=38.3/S02=38.8/S03=29.05, P001=33.55/P002=21.6/P003=10.5/P004=40.5) y con el mismo archivo de prueba del módulo 2, orders_2026-08-11.csv, con la única orden de la tienda inexistente S04. Lo que cambia es la forma de invocar ese trabajo. Hasta el módulo 6, la única manera de correr el pipeline era uv run python -m kiosko_pipeline, sin ningún argumento, siempre sobre la semana completa. Este módulo agrega una segunda forma, la que un equipo real usaría en producción — un scheduler (piensa en un cron, o en Airflow, la guía hermana de esta) que invoca, cada mañana, exactamente un comando con exactamente una fecha:

kiosko-pipeline --date 2026-08-05

Esa fecha nunca sale del reloj del sistema — es siempre un argumento explícito, escrito por quien invoca el comando (una persona, o la configuración de un scheduler). Vas a ver, en la lección 2, exactamente por qué esa distinción no es cosmética.

Una analogía: el día de la entrega, no un oficio más

Piensa en la construcción de una casa. El plomero instaló las cañerías (módulo 1: el paquete). El electricista cableó cada habitación (módulo 2: los tests, que confirman que cada cable lleva corriente a donde debe). El técnico de alarmas instaló los sensores (módulo 3: logging — un registro de lo que pasa en la casa, en vez de silencio). El cerrajero de seguridad instaló las chapas de emergencia (módulo 4: reintentos y cuarentena). Y dos ingenieros distintos, cada uno por su cuenta, certificaron que el sistema de climatización funciona con dos tecnologías distintas —una a gas, otra eléctrica— y ambas calientan la casa exactamente igual (módulos 5 y 6: DuckDB y Polars, probados en paralelo al dict original).

Cada oficio hizo su trabajo, y cada uno lo probó por su cuenta. Pero ninguno de ellos, individualmente, entrega la casa. La entrega es un momento distinto, con responsabilidades propias: alguien decide, con criterio, cuál sistema de climatización queda conectado de verdad (no los dos al mismo tiempo, ni ninguno). Alguien instala el timbre y la chapa de la puerta principal, para que el nuevo dueño entre por ahí —no saltando la ventana de la cocina, que es por donde entraban los obreros durante la construcción—. Y alguien firma un documento que dice, con evidencia, que la casa entera funciona junta: las luces se prenden con la corriente correcta, la alarma suena si hace falta, la puerta cierra con llave. Este módulo es ese día de entrega. No instala ningún oficio nuevo — conecta, con criterio explícito, lo que los seis módulos anteriores ya construyeron por separado.

Ejemplo trabajado: confirmando el punto de partida exacto

Antes de escribir una sola línea nueva, verifica, con tus propias manos, los dos huecos que abre este módulo. Primero, que no existe ninguna forma de pedirle al pipeline un solo día:

uv run python -m kiosko_pipeline

Qué esperar (treinta y una líneas; se muestran la primera, un salto, y las dos últimas — siempre la semana completa, nunca un día):

{"db_path": "data/warehouse.db", "event": "pipeline_run_started", "level": "info", "run_id": "kiosko-2026-08-03-2026-08-09", "timestamp": "2026-08-12T09:00:00Z", "week_days": ["2026-08-03", "2026-08-04", "2026-08-05", "2026-08-06", "2026-08-07", "2026-08-08", "2026-08-09"]}
...
{"event": "pipeline_run_completed", "level": "info", "quarantined_partitions": [], "rows_extracted": 40, "rows_loaded": 40, "rows_rejected": 0, "rows_valid": 40, "run_id": "kiosko-2026-08-03-2026-08-09", "timestamp": "2026-08-12T09:00:00Z", "week_revenue": 106.15}
{"event": "kiosko_weekly_report", "level": "info", "quarantined_partitions": [], "revenue_by_product": {"P001": 33.55, "P002": 21.6, "P003": 10.5, "P004": 40.5}, "revenue_by_store": {"S01": 38.3, "S02": 38.8, "S03": 29.05}, "rows_extracted": 40, "rows_loaded": 40, "rows_rejected": 0, "rows_valid": 40, "run_id": "kiosko-2026-08-03-2026-08-09", "timestamp": "2026-08-12T09:00:00Z", "week_revenue": 106.15}

No hay ningún --date, ningún argumento — WEEK_DAYS, la lista fija de siete fechas, vive escrita dentro de pipeline.py desde el módulo 1, y main() en __init__.py la usa sin preguntar nada. Segundo, confirma que pipeline.py sigue usando exclusivamente el motor original:

grep -n "^from kiosko_pipeline" src/kiosko_pipeline/pipeline.py

Qué esperar:

from kiosko_pipeline.extract import extract_orders, parse_order
from kiosko_pipeline.logging_config import get_pipeline_logger
from kiosko_pipeline.quality import validate_orders
from kiosko_pipeline.retry import DataQualityError, FlakyFault, load_to_warehouse_resilient, transform_or_quarantine
from kiosko_pipeline.transform import DIM_PRODUCT, DIM_STORE

Ni transform_duckdb ni transform_polars aparecen en esa lista — transform_or_quarantine(), importado de retry.py, sigue envolviendo el for con diccionarios original de transform.py, exactamente como lo dejó el módulo 4. Dos motores completos, probados, listos — y cero de los dos conectados a la corrida real. Ese es, con precisión, el punto de partida de este módulo.

Diagrama: qué agrega este módulo, y sobre qué se apoya

flowchart TD
    subgraph Existente["Modulos 1-6 -- sin cambiar su logica de negocio"]
        A["extract.py, quality.py,\ntransform.py, load.py"]
        B["logging_config.py\n(eventos JSON con contexto)"]
        C["retry.py\n(TransientLoadError, DataQualityError,\nload_retry, transform_or_quarantine)"]
        D["transform_duckdb.py\n(SQL, modulo 5)"]
        E["transform_polars.py\n(Polars, modulo 6)"]
        F["tests/ -- 35 tests"]
    end
    subgraph Nuevo["Modulo 7 -- el paquete que se entrega"]
        G["config.py\n(nuevo -- sin valores hardcodeados)"]
        H["cli.py\n(nuevo -- argparse, --date)"]
        I["retry.py\n(gana: transform_or_quarantine_polars)"]
        J["pipeline.py\n(gana: parametro engine)"]
        K["tests/test_pipeline_integration.py\n(nuevo -- integracion + idempotencia)"]
    end
    A --> J
    B --> H
    C --> I
    E -.la decision de este modulo.-> I
    D -.evaluada, no elegida.-> J
    G --> H
    I --> J
    J --> H
    H --> K
    J --> K

extract.py, quality.py, transform.py y load.py no se tocan en este módulo — su lógica de negocio queda exactamente donde la dejó el módulo 1. Lo que se agrega es una capa de entrega: config.py resuelve dónde viven los datos y el warehouse sin ningún valor hardcodeado; cli.py traduce un comando de terminal en una corrida real del pipeline, para una sola fecha; retry.py gana una función nueva que conecta el motor Polars al mismo contrato de cuarentena que ya conoces; y pipeline.py gana un parámetro (engine) que decide, en cada corrida, cuál de los dos motores probados calcula fact_orders — sin que ningún test de los treinta y cinco anteriores necesite cambiar una sola línea.

El mapa de este módulo

Leccion   Pregunta que contesta
────────  ──────────────────────────────────────────────────────
L1        (esta) Que le falta a un pipeline testeado para ser un paquete que se entrega
L2        Como convertir un comando de terminal en una corrida real, con argparse
L3        Como dejar de escribir "data" a mano en cada archivo del paquete
L4        Que motor corre en produccion, y como se conecta con logging y reintentos ya integrados
L5        Como probar el pipeline COMPLETO, no una funcion a la vez
L6        Como probar idempotencia con un assert, no corriendo el comando dos veces a ojo
L7        Que le faltaria pedir a este paquete un revisor de codigo real
L8        Proyecto: el paquete entregable de Kiosko, de punta a punta

Las lecciones 2 y 3 construyen, en ese orden, la superficie de entrada del paquete: primero el comando en sí —argparse, --date, el patrón main(argv=None) que hace posible testear un CLI sin lanzar un proceso nuevo por cada test (lección 2)—, después la eliminación de cualquier ruta escrita a mano dentro del código (lección 3). La lección 4 es el centro del módulo: aplica el criterio de la lección 7 del módulo 6 —cuándo conviene DuckDB, cuándo Polars— a la decisión real de kiosko_pipeline, conecta el motor elegido al mismo mecanismo de cuarentena que ya conoces desde el módulo 4, y descubre, en vivo, un caso límite que ningún test anterior había ejercitado. Las lecciones 5 y 6 automatizan la verificación que, hasta este punto, siempre hiciste a mano: correr un comando, leer la salida, comparar con lo que esperabas — ahora un pytest decide por ti, con assert, incluida la prueba de idempotencia central de esta guía. La lección 7 se detiene, a propósito, antes de construir nada de infraestructura, y nombra con honestidad qué le pediría un revisor de código real a este paquete. Y la lección 8 —el mini-proyecto— ensambla todo, corrido de punta a punta.

La frontera: qué NO cubre este módulo

Este módulo termina en el límite exacto de lo que corre en tu laptop: un comando instalado, configurado sin valores hardcodeados, con un motor de transformación elegido con criterio, probado con una suite completa incluida la idempotencia. Lo que pasa después de eso —que ese comando corra automáticamente en cada git push (integración continua), que corra dentro de un contenedor Docker en vez de tu entorno local, que se despliegue en un proveedor de nube concreto (AWS, GCP), que un orquestador como Airflow lo invoque con reintentos y sensores propios— es, con toda intención, terreno de otras guías del ecosistema: docker-essentials-guide, terraform-and-iac-guide y aws-core-services-guide para contenerización y despliegue concreto, airflow-and-declarative-orchestration-guide para orquestación real. La lección 7 de este módulo nombra cada una de estas piezas, con precisión, como lo que un revisor de código real pediría antes de aprobar este paquete para producción — pero no construye ninguna. "Producción", en esta guía, sigue significando lo que significó desde el módulo 1: testeado, logueado y seguro de reintentar en tu laptop, no desplegado en la nube.

Errores comunes

Pensar que este módulo es "solo agregar argparse". Qué pasa: alguien, al ver el título de la lección 2, asume que todo el módulo se reduce a envolver main() con un parser de línea de comandos, y que el resto son detalles menores. Por qué pasa: argparse es, genuinamente, la pieza más visible del módulo —es lo primero que se escribe—, y es fácil confundir "lo primero que ves" con "lo único que importa". Cómo detectarlo: si terminas este módulo sin poder explicar por qué pipeline.py gana un parámetro engine, o qué problema resuelve config.py que argparse por sí solo no resuelve, te falta la mitad del módulo. Cómo corregirlo: argparse (lección 2) es la puerta de entrada, literal — pero la decisión real de este módulo, la que un equipo de verdad discutiría en una revisión de código, es cuál motor de transformación corre cada vez que alguien toca esa puerta (lección 4), y cómo se prueba que el resultado es siempre el mismo sin importar cuántas veces se invoque (lecciones 5 y 6).

Asumir que "decidir el motor" es un ejercicio académico, sin consecuencias reales en el código. Qué pasa: alguien espera que la lección 4 sea, simplemente, "cambiar una línea de import" —de transform_fact_orders a transform_fact_orders_polars— y seguir adelante. Por qué pasa: los módulos 5 y 6 ya probaron, con tests, que los tres motores producen exactamente el mismo fact_orders — parece razonable asumir que intercambiarlos es trivial. Cómo detectarlo: si no sabes explicar qué le pasa a una orden de una tienda desconocida (S04) cuando el motor de transformación es un INNER JOIN en vez del for con diccionarios original, todavía no viste el problema real que resuelve la lección 4. Cómo corregirlo: un INNER JOIN —en SQL o en Polars— nunca lanza una excepción frente a una fila huérfana, la descarta en silencio; el mecanismo de cuarentena de este paquete, construido en el módulo 4, depende exactamente de que sí se lance una excepción. Conectar cualquiera de los dos motores nuevos a producción exige, con honestidad, resolver esa diferencia — no es una línea, es una decisión de diseño completa, y la lección 4 la resuelve en vivo.

Confundir "el comando corre en mi laptop" con "está listo para producción". Qué pasa: alguien termina este módulo, ve kiosko-pipeline --date 2026-08-05 funcionando, la suite completa en verde, y da por cerrada la pregunta de si este paquete está "listo". Por qué pasa: después de seis módulos construyendo hacia este momento, ver el comando funcionar se siente como la línea de meta. Cómo detectarlo: si no puedes nombrar, sin mirar la lección 7, al menos tres cosas concretas que un revisor de código pediría antes de aprobar este paquete para correr en producción de verdad (no en tu laptop), te falta esa lección. Cómo corregirlo: la lección 7 de este módulo existe exactamente para esto — nombra, con honestidad y sin construir nada, la distancia real entre "corre en mi laptop, testeado" y "un equipo lo aprobaría para producción": CI/CD, un contenedor, secretos, un cloud concreto, monitoreo. Cerrar ese hueco es, a propósito, trabajo de otras guías — pero saber nombrarlo es trabajo de este módulo.

Ejercicios

Ejercicio 1 — Confirma tu punto de partida, con tus propias manos. Desde la raíz de tu proyecto kiosko_pipeline/, corre uv run python -m kiosko_pipeline y confirma que obtienes las treinta y una líneas de siempre, terminando en "week_revenue": 106.15. Después, corre grep -n "engine" src/kiosko_pipeline/pipeline.py y confirma que no aparece ninguna coincidencia — la palabra engine todavía no existe en tu código.

Ver solución

Si tu proyecto quedó exactamente como lo dejó el módulo 6, deberías ver las treinta y una líneas de siempre, con "week_revenue": 106.15 en la última línea, y el grep de engine no debería encontrar ninguna coincidencia — pipeline.py, al cerrar el módulo 6, no tiene ningún concepto de "qué motor usar": siempre usa transform_or_quarantine(), el original basado en diccionarios, sin ninguna alternativa configurable. Ese es, con precisión, uno de los tres huecos que este módulo cierra.

Ejercicio 2 — Anticipa el problema del INNER JOIN silencioso, antes de la lección 4. Sin mirar la lección 4 todavía, y usando lo que ya sabes de los módulos 5 y 6 sobre cómo un INNER JOIN (en SQL o en Polars) maneja una fila huérfana, escribe en 2-3 frases qué esperarías que pasara si, hoy, conectaras transform_fact_orders_polars() directamente al lugar donde pipeline.py llama a transform_or_quarantine(), sin ningún cambio adicional, y corrieras el pipeline sobre una partición con la orden S04.

Ver solución

Si conectaras transform_fact_orders_polars() directamente, sin ningún cambio adicional, la orden S04 simplemente desaparecería del resultado, sin ningún aviso — un how="inner" (el join que usa esa función) descarta en silencio cualquier fila cuyo store_id no exista en dim_store, exactamente el comportamiento que la lección 6 del módulo 6 documentó con find_unregistered_stores_polars(). La partición no quedaría en cuarentena —porque nada lanzaría ninguna excepción que el try/except DataQualityError de pipeline.py pudiera capturar— y el revenue final sería, silenciosamente, más bajo de lo que debería ser, sin que quarantined_partitions lo reflejara. Ese hueco específico es exactamente lo que la lección 4 tiene que resolver antes de poder usar cualquiera de los dos motores nuevos en producción.

Ejercicio 3 — Explica la analogía de la entrega con tus propias palabras. En 2-3 frases, y sin mirar esta lección otra vez, explica por qué "entregar la casa" es una etapa distinta de "cada oficio hizo bien su trabajo", conectándolo con al menos dos de los seis módulos anteriores de esta guía.

Ver solución

Cada módulo anterior construyó y probó una pieza completa, de forma aislada: el módulo 2 confirmó, con diecinueve tests, que extract_orders(), validate_orders() y transform_fact_orders() calculan lo correcto; el módulo 4 confirmó, con cinco tests deterministas, que el reintento y la cuarentena funcionan cada uno por su cuenta. Pero ninguna de esas pruebas confirmó que las piezas funcionan juntas, invocadas de la forma en que un equipo real las invocaría —un comando de terminal, con una fecha específica, sin que nadie edite código para cambiar qué día se procesa—. "Entregar la casa" es exactamente esa etapa distinta: no repetir el trabajo de cada oficio, sino conectarlos, con una decisión explícita sobre qué queda conectado a qué, y confirmar, con una prueba nueva, que el conjunto completo funciona como una sola cosa.

Resumen y siguiente paso

En esta lección confirmaste, con tus propias manos, el punto exacto donde el módulo 6 dejó a kiosko_pipeline: treinta y cinco tests en verde, tres motores de transformación completos y de acuerdo hasta el último centavo, pero sin ninguna forma de pedirle al pipeline un solo día específico, con rutas hardcodeadas dentro del código, y sin que ningún motor nuevo esté conectado a la corrida real. Viste el mapa de las ocho lecciones que cierran esos tres huecos, y la frontera honesta de lo que este módulo — y esta guía — no construye: CI/CD, contenedores, un cloud concreto.

Antes de avanzar deberías poder: nombrar los tres huecos concretos que abre esta lección (sin --date, rutas hardcodeadas, ningún motor nuevo conectado); explicar por qué un INNER JOIN silencioso es un problema real para el mecanismo de cuarentena del módulo 4, no un detalle menor; y ubicar, en el mapa de ocho lecciones, dónde se resuelve cada uno.

La lección 2 construye la primera pieza: un entry point de línea de comandos real, con argparse, que traduce kiosko-pipeline --date 2026-08-05 en una corrida concreta del pipeline — la puerta principal del paquete, con timbre, en vez de la ventana por la que entraban los módulos anteriores llamando funciones internas directamente desde una sesión de Python.

Recursos

  • Python — documentación oficial del módulo argparse, la referencia central de la lección 2 de este módulo. docs.python.org/3/library/argparse.html. En inglés.
  • pytest — documentación oficial, la referencia de las lecciones 5 y 6, donde el pipeline completo se prueba de punta a punta. docs.pytest.org/en/stable. En inglés.
  • uv — referencia oficial de [project.scripts], el mecanismo que este módulo reconecta a un archivo nuevo (cli.py) en la lección 2. docs.astral.sh/uv/concepts/projects/config/#entry-points. En inglés.
  • DISEÑO de python-for-data-engineering-guide — el mapa completo de los ocho módulos de esta guía, incluida la frontera con docker-essentials-guide, terraform-and-iac-guide, aws-core-services-guide y airflow-and-declarative-orchestration-guide que retoma la lección 7. src/guides/python-for-data-engineering-guide/DISENO.md. En español.