Autor: Santiago Colmenares Bolivar
Sistema backend para la recepción, procesamiento y almacenamiento de archivos CSV con registros de ventas. El sistema está diseñado de forma modular: una API construida con FastAPI que expone endpoints para la carga de archivos y consulta de estados, almacenamiento en Azure Blob Storage, procesamiento asíncrono mediante un Worker que escucha una cola de mensajes, y automatización de reportes con N8N.
┌─────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ Cliente │────>│ FastAPI API │───>│ Azure Blob │
│ (CSV file) │ │ POST /upload │ │ Storage │
└─────────────┘ │ GET /job/{id} │ │ (streaming) │
└────────┬──────────┘ └──────────────────┘
│ │
v │
┌──────────────────┐ │
│ Azure Storage │ │
│ Queue │ │
└────────┬─────────┘ │
│ │
v v
┌───────────────────────────────────────┐
│ Worker (Python) │
│ - Escucha la cola (polling) │
│ - Descarga CSV como stream (chunks) │
│ - Parsea línea por línea │
│ - Inserta con execute_values │
│ - Lotes de 5000 filas │
└────────────────┬──────────────────────┘
│
v
┌───────────────────────────────────────┐
│ PostgreSQL │
│ - jobs (estado del procesamiento) │
│ - sales (registros de ventas) │
│ - sales_daily_summary (resumen N8N) │
└────────────────┬──────────────────────┘
│
v
┌───────────────────────────────────────┐
│ N8N Workflow │
│ - Consulta jobs completados │
│ - Calcula total de ventas por día │
│ - Guarda en sales_daily_summary │
└───────────────────────────────────────┘
Se utilizó la librería psycopg2 para la conexión con PostgreSQL con el fin de realizar las consultas SQL lo más claras posible. Una alternativa era SQLAlchemy, sin embargo su modelo ORM agrega complejidad innecesaria para este caso de uso. Con psycopg2 se tiene control directo sobre las queries SQL, lo cual facilita la optimización de inserciones masivas con execute_values.
El procesamiento se realiza en dos niveles de streaming:
-
Upload (API): El archivo se sube a Azure Blob Storage directamente desde el
SpooledTemporaryFilede FastAPI, sin hacerfile.read()que cargaría todo en RAM. -
Descarga y procesamiento (Worker): El blob se descarga usando
download_blob().chunks()que entrega pedazos de 4MB. Cada chunk se parsea línea por línea, acumulando filas en un batch de 5,000. Cuando el batch está lleno se inserta y se libera la memoria. De esta forma el consumo de memoria se mantiene constante sin importar el tamaño del archivo.
Este enfoque lo basé en un programa que realicé con microcontroladores donde tenía que pasar datos entre dos memorias SD. No era a la escala de 5,000 líneas pero sí el mismo método: leer un pedazo, procesarlo, guardarlo, y seguir con el siguiente. Con esto se evitaba el overflow de memoria en el microcontrolador.
-
Inserción masiva con
execute_values: En vez de usarexecutemany(que internamente hace un INSERT por fila), se usapsycopg2.extras.execute_valuesque genera un solo INSERT con múltiples VALUES. Esto es ~10x más rápido y reduce la carga en PostgreSQL. -
Commit por batch: Se hace commit después de cada lote de 5,000 filas, no al final de todo el archivo. Si algo falla a la mitad, los lotes ya insertados no se pierden.
-
Procesamiento secuencial: La cola procesa un mensaje a la vez (
max_messages=1), evitando múltiples escrituras simultáneas. -
Visibility timeout: Se usa un timeout de 300 segundos para evitar que otro worker tome el mismo mensaje si el procesamiento es largo.
Se optó por Azure Storage Queue por ser más simple y suficiente para este caso de uso. El worker utiliza polling (consulta la cola cada 2 segundos) similar al modelo de polling en sistemas embebidos. Como mejora futura se podría migrar a Azure Service Bus que soporta un modelo basado en eventos (push), equivalente a trabajar con interrupciones en vez de polling, lo cual reduciría la latencia de respuesta.
Todas las variables de configuración (PostgreSQL, Azure, worker) están centralizadas en app/config.py y se cargan desde variables de entorno con valores por defecto para desarrollo local. Esto permite cambiar entre entornos (local, Docker, producción) sin modificar código.
Para desarrollo local se utiliza Azurite como emulador de Azure Storage. El sistema está diseñado para funcionar con Azure Blob Storage y Azure Storage Queue en producción, únicamente cambiando la variable de entorno AZURE_STORAGE_CONNECTION_STRING.
PRUEBALOGYCA/
├── app/
│ ├── __init__.py
│ ├── main.py # Punto de entrada FastAPI
│ ├── config.py # Configuración centralizada (env vars)
│ ├── api/
│ │ ├── __init__.py
│ │ └── api_conexion.py # Endpoints (upload, job status)
│ ├── db/
│ │ ├── __init__.py
│ │ └── creacion_SQL.py # Conexión y creación de tablas
│ ├── services/
│ │ ├── __init__.py
│ │ ├── blob_services_azure.py # Upload/download streaming Azure Blob
│ │ ├── queue_service.py # Envío de mensajes a la cola
│ │ └── worker.py # Procesamiento asíncrono del CSV
│ └── tests/
│ ├── __init__.py
│ ├── test_routes.py # Tests de endpoints (con mocks)
│ └── test_worker.py # Tests del worker (con mocks)
├── data/
│ ├── generate_sample.py # Generador de CSV de prueba
│ └── sample.csv # CSV de ejemplo (100,000 registros)
├── n8n/
│ └── workflow.json # Export del workflow de N8N
├── .env.example # Variables de entorno (Opción 2: Docker + Local)
├── .env.docker.example # Variables de entorno (Opción 1: Todo Docker)
├── .env.local.example # Variables de entorno (Opción 3: Todo Local)
├── .gitignore
├── docker-compose.yml
├── Dockerfile
├── requirements.txt
└── README.md
- Python 3.10+
- Docker Desktop
- PostgreSQL 16+ (solo si se ejecuta con la Opción 3, sin Docker)
git clone https://github.com/stg077/PruebaLOGYCA.git
cd PruebaLOGYCAAntes de ejecutar cualquier opción, copia el archivo de ejemplo correspondiente:
| Opción | Comando |
|---|---|
| 1 (Todo Docker) | cp .env.docker.example .env.docker |
| 2 (Docker + Local) | cp .env.example .env |
| 3 (Todo Local) | cp .env.local.example .env |
Nota sobre puertos: PostgreSQL en Docker se expone en el puerto 5433 del host para evitar conflictos con instalaciones locales de PostgreSQL (que usan 5432).
# Crear el entorno virtual
python3 -m venv venv
# Activar el entorno
source venv/bin/activate # Linux / macOS
# venv\Scripts\activate # Windows (PowerShell/CMD)
# Instalar dependencias
pip install --upgrade pip
pip install -r requirements.txtEn caso de que solo descargue el comprimido del proyecto, corrobore que esté dentro de la raíz del proyecto para realizar todo de la mejor manera.
Este comando levanta todo el ecosistema: PostgreSQL, Azurite, n8n, la API y el Worker.
Nota: Si tienes un entorno virtual activo, puedes desactivarlo con el comando deactivate antes de correr Docker para evitar confusiones de rutas, aunque no es estrictamente necesario.
docker-compose up -d --buildNo se necesita iniciar nada más manualmente.
La API queda disponible en http://localhost:8000/docs.
Una vez que los contenedores estén corriendo, puedes pasar directamente a la sección de Probar el flujo Completo.
Levanta solo PostgreSQL, Azurite y N8N con Docker, y ejecuta la API y el Worker localmente.
docker-compose up -d postgres azurite n8nuvicorn app.main:app --reloadLa API estará disponible en http://localhost:8000/docs
python -m app.services.workerUna vez que los contenedores estén corriendo, puedes pasar directamente a la sección de Probar el flujo Completo.
Si no se quiere usar docker-compose, se pueden levantar los servicios individualmente con Docker.
En pgAdmin o psql, crear la base de datos:
CREATE DATABASE logyca_sales;docker run -d --name azurite -p 10000:10000 -p 10001:10001 -p 10002:10002 mcr.microsoft.com/azure-storage/azurite azurite --skipApiVersionCheck --blobHost 0.0.0.0 --queueHost 0.0.0.0docker run -d --name n8n -p 5678:5678 -e N8N_HOST=localhost -e WEBHOOK_URL=http://localhost:5678/ n8nio/n8nuvicorn app.main:app --reloadpython -m app.services.worker-
Generar CSV de prueba:
python data/generate_sample.py -
Subir el CSV:
POST http://localhost:8000/upload(usar Swagger UI o curl) -
Verificar estado:
GET http://localhost:8000/job/{job_id} -
El worker procesa automáticamente y el status cambia a COMPLETED
-
Configurar N8N:
- Abrir
http://localhost:5678en el navegador - Ir a Workflows > Import from File
- Seleccionar el archivo
n8n/workflow.jsonincluido en el proyecto - Configurar las credenciales de PostgreSQL en N8N:
-
Base de datos:
logyca_sales -
Usuario:
postgres -
Contraseña:
postgres -
Host y Puerto según la opción:
Opción Host Puerto 1 postgres54322 host.docker.internal54333 host.docker.internal5432
-
- Activar el workflow para que ejecute periódicamente y genere el resumen diario en
sales_daily_summary
- Abrir
-
Para validar la ejecucion de el WorkFlow de n8n ejecutar
http://localhost:8000/statsEste traera una breve resumen de las base de datos integradas
Se recomienda utilizar el Swagger UI (http://localhost:8000/docs) para que el proceso de validación sea más sencillo.
pytest app/tests/ -vLas pruebas usan mocks y no requieren servicios externos corriendo.
| Método | Ruta | Descripción |
|---|---|---|
| GET | / | Validacion inicio de API |
| POST | /upload | Sube un CSV, lo almacena en Blob y encola su procesamiento |
| GET | /job/{job_id} | Consulta el estado de un job (PENDING, PROCESSING, COMPLETED, FAILED) |
| GET | /stats | Consulta las bases de Datos retornado un breve resumen de las mismas |
Recibe un archivo CSV con la estructura:
date,product_id,quantity,price
2026-01-01,1001,2,10.5
2026-01-01,1002,1,5.2Respuesta:
{
"job_id": "uuid-del-job",
"status": "PENDING"
}Validaciones:
- Solo acepta archivos
.csv - Verifica que el CSV contenga las columnas requeridas (date, product_id, quantity, price)
- Rechaza archivos vacíos
Respuesta:
{
"job_id": "uuid-del-job",
"file_name": "ventas.csv",
"status": "COMPLETED",
"created_at": "2026-01-15 10:30:00"
}Respuesta:
{
"total_sales_records": 300000,
"total_jobs": 4,
"completed_jobs": 3,
"sales_daily_summary_rows": 91,
"sales_daily_summary_sample": [
{
"id": 50,
"date": "2026-01-01",
"total_sales": 1821138.26,
"total_quantity": 34674,
"record_count": 3294,
"updated_at": "2026-04-02T12:45:58.569438"
},
{
"id": 34,
"date": "2026-01-02",
"total_sales": 1886160.99,
"total_quantity": 35463,
"record_count": 3386,
"updated_at": "2026-04-02T12:45:58.569438"
},
{(....)}
}
Almacena el estado de cada archivo subido.
| Campo | Tipo | Descripción |
|---|---|---|
| id | UUID | Identificador único del job |
| file_name | VARCHAR | Nombre del archivo original |
| blob_path | VARCHAR | Ruta en Azure Blob Storage |
| status | VARCHAR | PENDING, PROCESSING, COMPLETED, FAILED |
| error_message | TEXT | Mensaje de error si falló |
| created_at | TIMESTAMP | Fecha de creación |
| updated_at | TIMESTAMP | Última actualización |
Almacena cada registro de venta procesado.
| Campo | Tipo | Descripción |
|---|---|---|
| id | SERIAL | Identificador autoincremental |
| job_id | UUID | Referencia al job que lo procesó |
| date | DATE | Fecha de la venta |
| product_id | INTEGER | ID del producto |
| quantity | INTEGER | Cantidad vendida |
| price | NUMERIC(10,2) | Precio unitario |
| total | NUMERIC(12,2) | quantity * price |
Resumen diario generado por N8N.
| Campo | Tipo | Descripción |
|---|---|---|
| id | SERIAL | Identificador autoincremental |
| date | DATE | Fecha (única) |
| total_sales | NUMERIC(14,2) | Suma de ventas del día |
| total_quantity | INTEGER | Suma de cantidades del día |
| record_count | INTEGER | Número de registros del día |
| updated_at | TIMESTAMP | Última actualización |
- FastAPI — Framework para la API REST
- PostgreSQL 16 — Base de datos relacional
- psycopg2 — Driver de PostgreSQL con soporte para inserción masiva (execute_values)
- Azure Blob Storage — Almacenamiento de archivos CSV (Azurite para local)
- Azure Storage Queue — Cola de mensajes para procesamiento asíncrono
- N8N — Automatización del workflow de reportes diarios
- Docker — Contenerización y orquestación de servicios