13 KiB
MVE Incrementables Parser
Microservicio para extraer y parsear la sección de Incrementables de documentos PDF tipo "CARTA INS". Construido con FastAPI y Python 3.12.
🚀 Características
- ✅ Autenticación JWT con bcrypt
- ✅ Extracción de texto con PyMuPDF y pdfplumber (fallback)
- ✅ Parsing robusto de incrementables (fletes, seguros, almacenaje, regalías)
- ✅ Validación de archivos (tamaño, tipo MIME, PDF cifrado)
- ✅ Logging estructurado con correlation ID
- ✅ Rate limiting en login
- ✅ Procesamiento asíncrono con Celery y Redis
- ✅ Cola de tareas para procesamiento en background
- ✅ Tests con pytest
- ✅ Docker & Docker Compose
📋 Requisitos
- Python 3.12+
- Docker & Docker Compose (opcional)
🛠️ Instalación
Opción 1: Con Docker (Recomendado)
- Clonar el repositorio
cd mve-incrementables-parser
- Configurar variables de entorno
cp .env.example .env
Edita .env y configura:
AUTH_USERNAME=admin
AUTH_PASSWORD_HASH=$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/LewY5GyYqwMzYXhHO
JWT_SECRET=your-secret-key-change-this-in-production
JWT_EXPIRES_MINUTES=60
MAX_FILE_MB=10
LOG_LEVEL=INFO
Generar hash de contraseña:
python -c "from passlib.hash import bcrypt; print(bcrypt.hash('tu_contraseña'))"
- Levantar el servicio
docker-compose up --build
El servicio estará disponible en http://localhost:9876
Servicios incluidos:
- API FastAPI: puerto 9876
- Redis: puerto 16379
- Celery Worker: procesamiento en background
Opción 2: Sin Docker
- Instalar dependencias del sistema (para PyMuPDF)
# macOS
brew install mupdf-tools
# Ubuntu/Debian
sudo apt-get install libmupdf-dev mupdf-tools
- Crear entorno virtual
python3.12 -m venv venv
source venv/bin/activate # En Windows: venv\Scripts\activate
- Instalar dependencias Python
pip install -r requirements.txt
-
Configurar .env (ver Opción 1, paso 2)
-
Ejecutar el servicio
uvicorn app.main:app --host 0.0.0.0 --port 9876 --reload
📚 Uso
1. Health Check
curl http://localhost:9876/health
Respuesta:
{
"status": "ok",
"service": "mve-incrementables-parser",
"version": "1.0.0"
}
2. Autenticación (Login)
curl -X POST http://localhost:9876/auth/login \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "tu_contraseña"
}'
Respuesta:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3600
}
⚠️ Nota: Guarda el access_token para usarlo en las siguientes peticiones.
3. Parsear PDF (Síncrono)
curl -X POST http://localhost:9876/v1/incrementables/parse \
-H "Authorization: Bearer <TU_TOKEN_AQUI>" \
-F "file=@CARTA_INS_228718.pdf" \
-F "document_ref=INS-228718"
Con Correlation ID personalizado:
curl -X POST http://localhost:9876/v1/incrementables/parse \
-H "Authorization: Bearer <TU_TOKEN_AQUI>" \
-H "X-Correlation-Id: custom-id-12345" \
-F "file=@CARTA_INS_228718.pdf"
Respuesta exitosa (200):
{
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"document": {
"filename": "CARTA INS 228718.pdf",
"pages": 1,
"sha256": "a3d5f..."
},
"incrementables": {
"currency": "USD",
"fletes": 1591.20,
"seguros": null,
"almacenaje_consolidacion": 0.00,
"regalias": null
},
"extraction": {
"method": "text",
"anchors_found": ["AJUSTE DE INCREMENTABLES EN:"],
"warnings": []
}
}
4. Parsear PDF (Asíncrono con Cola)
Para archivos grandes o cuando no quieres esperar, usa el endpoint asíncrono:
curl -X POST http://localhost:9876/v1/incrementables/parse/async \
-H "Authorization: Bearer <TU_TOKEN_AQUI>" \
-F "file=@CARTA_INS_228718.pdf" \
-F "document_ref=INS-228718"
Respuesta (202):
{
"task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"message": "Task queued for processing. Use /v1/incrementables/status/{task_id} to check progress."
}
5. Consultar Estado de Tarea
curl -X GET http://localhost:9876/v1/incrementables/status/a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c \
-H "Authorization: Bearer <TU_TOKEN_AQUI>"
Respuesta (en proceso):
{
"task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c",
"status": "started",
"result": null,
"error": null
}
Respuesta (completada):
{
"task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c",
"status": "completed",
"result": {
"status": "completed",
"task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c",
"document": {
"filename": "CARTA INS 228718.pdf",
"pages": 1,
"sha256": "a3d5f...",
"document_ref": "INS-228718"
},
"incrementables": {
"currency": "USD",
"fletes": 1591.20,
"seguros": null,
"almacenaje_consolidacion": 0.00,
"regalias": null
},
"extraction": {
"method": "text",
"anchors_found": ["AJUSTE DE INCREMENTABLES EN:"],
"warnings": []
}
},
"error": null
}
Errores comunes:
- 400: Archivo no válido, no es PDF, excede tamaño máximo
- 401: Token ausente, inválido o expirado
- 422: No se encontró sección de incrementables o parsing falló
- 500: Error interno del servidor
🔄 Procesamiento Asíncrono
El servicio soporta dos modos de procesamiento:
Modo Síncrono (/parse)
- Respuesta inmediata
- Ideal para PDFs pequeños
- Timeout en request HTTP
Modo Asíncrono (/parse/async)
- Respuesta inmediata con task_id
- Procesamiento en background con Celery
- Ideal para PDFs grandes o lotes
- Sin timeout (límite: 5 minutos por tarea)
- Consulta estado con
/status/{task_id}
Arquitectura
Cliente → FastAPI → Redis (Cola) → Celery Worker → Procesa PDF → Redis (Resultado)
↓
Cliente ← FastAPI ← Redis (Consulta) ←────────────────────────────────
🧪 Tests
# Ejecutar todos los tests
pytest
# Con cobertura
pytest --cov=app --cov-report=html
# Test específico
pytest tests/test_parser.py -v
# Ver logs detallados
pytest -v -s
🏗️ Estructura del Proyecto
mve-incrementables-parser/
├── app/
│ ├── __init__.py
│ ├── main.py # FastAPI app principal
│ ├── schemas.py # Modelos Pydantic
│ ├── core/
│ │ ├── config.py # Configuración (ENV)
│ │ ├── security.py # JWT, bcrypt, rate limiting
│ │ └── celery_app.py # Configuración de Celery
│ ├── api/
│ │ ├── auth.py # Endpoint de login
│ │ └── v1/
│ │ └── incrementables.py # Endpoints de parsing (sync/async)
│ ├── tasks/
│ │ └── parse_tasks.py # Tareas de Celery
│ └── services/
│ ├── pdf_text.py # Extracción de texto (PyMuPDF/pdfplumber)
│ └── parser.py # Parsing de incrementables
├── tests/
│ ├── conftest.py # Fixtures de pytest
│ ├── test_parser.py # Tests del parser
│ └── test_api.py # Tests de endpoints
├── .env.example # Ejemplo de variables de entorno
├── .gitignore
├── Dockerfile
├── docker-compose.yml # API + Redis + Celery Worker
├── pytest.ini
├── requirements.txt
└── README.md
│ │ └── v1/ │ │ └── incrementables.py # Endpoint de parsing │ └── services/ │ ├── pdf_text.py # Extracción de texto (PyMuPDF/pdfplumber) │ └── parser.py # Parsing de incrementables ├── tests/ │ ├── conftest.py # Fixtures de pytest │ ├── test_parser.py # Tests del parser │ └── test_api.py # Tests de endpoints ├── .env.example # Ejemplo de variables de entorno ├── .gitignore ├── Dockerfile ├── docker-compose.yml ├── pytest.ini ├── requirements.txt └── README.md
## 🔒 Seguridad
### Autenticación JWT
- **Login** con username/password → devuelve JWT
- **Protección** de endpoints `/v1/*` con Bearer token
- **Expiración** configurable (default: 60 minutos)
- **Rate limiting** básico en login (5 intentos / 5 minutos por IP)
### Contraseñas
- **Nunca** se loguean contraseñas en texto plano
- Uso de **bcrypt** para hashing
- Respuestas **uniformes** (401) en error de autenticación
### Archivos
- Validación de **tipo MIME**
- Validación de **extensión** `.pdf`
- **Tamaño máximo** configurable (default: 10 MB)
- Rechazo de **PDFs cifrados**
- Cálculo de **SHA256** para integridad
## ⚙️ Configuración (Variables de Entorno)
| Variable | Descripción | Default |
|----------|-------------|---------|
| `AUTH_USERNAME` | Usuario para login | `admin` |
| `AUTH_PASSWORD_HASH` | Hash bcrypt de contraseña | *requerido* |
| `JWT_SECRET` | Secret para firmar JWT | *requerido* |
| `JWT_EXPIRES_MINUTES` | Duración del token (minutos) | `60` |
| `MAX_FILE_MB` | Tamaño máximo de PDF (MB) | `10` |
| `LOG_LEVEL` | Nivel de logging | `INFO` |
| `REDIS_URL` | URL de conexión a Redis | `redis://redis:6379/0` |
| `CELERY_BROKER_URL` | URL del broker de Celery | `redis://redis:6379/0` |
| `CELERY_RESULT_BACKEND` | URL del backend de resultados | `redis://redis:6379/0` |
| `SERVICE_NAME` | Nombre del servicio | `mve-incrementables-parser` |
| `SERVICE_VERSION` | Versión del servicio | `1.0.0` |
## 📖 API Documentation
Una vez levantado el servicio, accede a la documentación interactiva:
- **Swagger UI**: http://localhost:9876/docs
- **ReDoc**: http://localhost:9876/redoc
## 🔍 Logging
El servicio utiliza **logging estructurado en JSON**:
```json
{
"time": "2026-03-02T10:30:45",
"level": "INFO",
"name": "app.api.v1.incrementables",
"message": "Successfully parsed incrementables",
"correlation_id": "550e8400-e29b-41d4-a716-446655440000",
"currency": "USD",
"fletes": 1591.20
}
Correlation ID
- Automático: Se genera UUID si no se proporciona
- Manual: Enviar header
X-Correlation-Id - Propagación: Aparece en logs y respuesta
- Utilidad: Tracking de requests en sistemas distribuidos
🐛 Troubleshooting
Error: "PyMuPDF extraction failed"
Causa: PDF corrupto o cifrado
Solución: El servicio intentará con pdfplumber automáticamente. Si ambos fallan:
- Verificar que el PDF no esté cifrado
- Intentar con otro PDF
- Revisar logs para detalles
Error: "Incrementables section not found"
Causa: El PDF no contiene la sección esperada o el formato es diferente
Solución:
- Verificar que el PDF tenga el texto "AJUSTE DE INCREMENTABLES EN:"
- Revisar el formato del documento
- Si es un formato nuevo, actualizar los patrones en
app/services/parser.py
Error: "Rate limit exceeded"
Causa: Demasiados intentos de login desde la misma IP
Solución: Esperar 5 minutos o reiniciar el servicio
🚀 Despliegue en Producción
Recomendaciones
- Cambiar credenciales: Generar nueva contraseña y JWT_SECRET
- HTTPS: Usar reverse proxy (nginx, traefik)
- Límites: Ajustar
MAX_FILE_MBsegún necesidad - Logging: Integrar con sistema de logs centralizado
- Monitoring: Configurar health checks y alertas
- Recursos: Ajustar memoria/CPU del contenedor
Ejemplo con nginx
server {
listen 443 ssl;
server_name api.mve.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:9876;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Aumentar límites para PDFs grandes
client_max_body_size 20M;
}
}
📝 Ejemplos de Formato Esperado
El servicio espera PDFs con formato similar a:
CARTA INSTRUCTIVO DE EMBARQUE No. 228718
AJUSTE DE INCREMENTABLES EN:
Fletes: $1,591.20 USD
Seguros: USD
Almacenaje/Consolidación: $0.00 USD
Regalías: USD
Observaciones...
Campos Reconocidos
- Fletes: Requerido, con o sin signo
$, con comas - Seguros: Opcional, puede estar vacío (solo "USD" →
null) - Almacenaje/Consolidación: Requerido, acepta variaciones de escritura
- Regalías/Regalias: Opcional, con o sin acento
🤝 Contribuir
- Fork el proyecto
- Crear rama feature (
git checkout -b feature/amazing-feature) - Commit cambios (
git commit -m 'Add amazing feature') - Push a la rama (
git push origin feature/amazing-feature) - Abrir Pull Request
📄 Licencia
Este proyecto es parte del sistema MVE y es de uso interno.
📧 Contacto
Para soporte o dudas, contactar al equipo de desarrollo MVE.
Versión: 1.0.0
Última actualización: Marzo 2026