512 lines
13 KiB
Markdown
512 lines
13 KiB
Markdown
# 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)
|
|
|
|
1. **Clonar el repositorio**
|
|
```bash
|
|
cd mve-incrementables-parser
|
|
```
|
|
|
|
2. **Configurar variables de entorno**
|
|
```bash
|
|
cp .env.example .env
|
|
```
|
|
|
|
Edita `.env` y configura:
|
|
```env
|
|
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:**
|
|
```bash
|
|
python -c "from passlib.hash import bcrypt; print(bcrypt.hash('tu_contraseña'))"
|
|
```
|
|
|
|
3. **Levantar el servicio**
|
|
```bash
|
|
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
|
|
|
|
1. **Instalar dependencias del sistema** (para PyMuPDF)
|
|
```bash
|
|
# macOS
|
|
brew install mupdf-tools
|
|
|
|
# Ubuntu/Debian
|
|
sudo apt-get install libmupdf-dev mupdf-tools
|
|
```
|
|
|
|
2. **Crear entorno virtual**
|
|
```bash
|
|
python3.12 -m venv venv
|
|
source venv/bin/activate # En Windows: venv\Scripts\activate
|
|
```
|
|
|
|
3. **Instalar dependencias Python**
|
|
```bash
|
|
pip install -r requirements.txt
|
|
```
|
|
|
|
4. **Configurar .env** (ver Opción 1, paso 2)
|
|
|
|
5. **Ejecutar el servicio**
|
|
```bash
|
|
uvicorn app.main:app --host 0.0.0.0 --port 9876 --reload
|
|
```
|
|
|
|
## 📚 Uso
|
|
|
|
### 1. Health Check
|
|
|
|
```bash
|
|
curl http://localhost:9876/health
|
|
```
|
|
|
|
**Respuesta:**
|
|
```json
|
|
{
|
|
"status": "ok",
|
|
"service": "mve-incrementables-parser",
|
|
"version": "1.0.0"
|
|
}
|
|
```
|
|
|
|
### 2. Autenticación (Login)
|
|
|
|
```bash
|
|
curl -X POST http://localhost:9876/auth/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"username": "admin",
|
|
"password": "tu_contraseña"
|
|
}'
|
|
```
|
|
|
|
**Respuesta:**
|
|
```json
|
|
{
|
|
"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)
|
|
|
|
```bash
|
|
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:**
|
|
```bash
|
|
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):**
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```bash
|
|
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):**
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
curl -X GET http://localhost:9876/v1/incrementables/status/a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c \
|
|
-H "Authorization: Bearer <TU_TOKEN_AQUI>"
|
|
```
|
|
|
|
**Respuesta (en proceso):**
|
|
```json
|
|
{
|
|
"task_id": "a8f2e9c1-5b3d-4e7f-9a1c-2d3e4f5a6b7c",
|
|
"status": "started",
|
|
"result": null,
|
|
"error": null
|
|
}
|
|
```
|
|
|
|
**Respuesta (completada):**
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
1. **Cambiar credenciales**: Generar nueva contraseña y JWT_SECRET
|
|
2. **HTTPS**: Usar reverse proxy (nginx, traefik)
|
|
3. **Límites**: Ajustar `MAX_FILE_MB` según necesidad
|
|
4. **Logging**: Integrar con sistema de logs centralizado
|
|
5. **Monitoring**: Configurar health checks y alertas
|
|
6. **Recursos**: Ajustar memoria/CPU del contenedor
|
|
|
|
### Ejemplo con nginx
|
|
|
|
```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
|
|
|
|
1. Fork el proyecto
|
|
2. Crear rama feature (`git checkout -b feature/amazing-feature`)
|
|
3. Commit cambios (`git commit -m 'Add amazing feature'`)
|
|
4. Push a la rama (`git push origin feature/amazing-feature`)
|
|
5. 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
|