Mejorado del Log de Procesador de Emails a Cronología
This commit is contained in:
parent
f0f45df1b8
commit
4fa955f71b
|
@ -0,0 +1,4 @@
|
||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
Puedes usar .doc\MemoriaDeEvolucion.md para obtener un contexto de las ultimas modificaciones y conceptos sobre este proyecto. Quisiera que con los conocimientos importantes y desiciones importantes adquiridas en cada modificacion los agregues a MemoriaDeEvolucion.md manteniendo el estilo que ya tenemos de texto simple sin demasiado codigo y una semantica resumida.
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Memoria de Evolución - Procesador de Emails a Cronología
|
||||||
|
|
||||||
|
## Descripcion de los scripts Procesador de Emails a Cronología
|
||||||
|
Este script procesa archivos de correo electrónico (.eml) para extraer su contenido, gestionar adjuntos y generar un archivo Markdown que presenta los mensajes en orden cronológico inverso.
|
||||||
|
|
||||||
|
Lógica Principal:
|
||||||
|
|
||||||
|
Beautify: Carga reglas de embellecimiento de texto desde config/beautify_rules.json para limpiar el contenido de los correos.
|
||||||
|
Descubrimiento: Busca todos los archivos .eml en el directorio de trabajo configurado.
|
||||||
|
|
||||||
|
Procesamiento Individual:
|
||||||
|
Itera sobre cada archivo .eml encontrado.
|
||||||
|
Utiliza utils.email_parser.procesar_eml para extraer metadatos (fecha, asunto, remitente, destinatarios), contenido del cuerpo y guardar los archivos adjuntos en la carpeta especificada.
|
||||||
|
Calcula un hash para cada mensaje para detectar duplicados.
|
||||||
|
|
||||||
|
Si un mensaje es nuevo (no duplicado):
|
||||||
|
|
||||||
|
Aplica las reglas de BeautifyProcessor al contenido del cuerpo.
|
||||||
|
Añade el mensaje procesado a una lista.
|
||||||
|
|
||||||
|
Ordenación: Ordena la lista de mensajes únicos por fecha, del más reciente al más antiguo.
|
||||||
|
|
||||||
|
Generación de Índice: Crea una sección de índice en formato Markdown con enlaces internos a cada mensaje.
|
||||||
|
Salida Markdown: Escribe el índice seguido del contenido formateado en Markdown de cada mensaje en el archivo de salida configurado (ej. cronologia.md).
|
||||||
|
|
|
@ -0,0 +1,410 @@
|
||||||
|
# Guía de Configuración para Scripts Backend
|
||||||
|
|
||||||
|
## Introducción
|
||||||
|
|
||||||
|
Esta guía explica cómo usar la función `load_configuration()` para cargar parámetros desde un archivo `script_config.json` en los scripts del backend.
|
||||||
|
|
||||||
|
## 1. Configuración del Script
|
||||||
|
|
||||||
|
Para que tus scripts puedan encontrar los módulos del proyecto, necesitas añadir el directorio raíz al path de Python.
|
||||||
|
|
||||||
|
### Path Setup e Importación
|
||||||
|
|
||||||
|
Coloca este código al inicio de tu script:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Añadir el directorio raíz al sys.path
|
||||||
|
# El número de `os.path.dirname()` depende de la profundidad del script.
|
||||||
|
# Para /backend/script_groups/grupo/script.py, son 4.
|
||||||
|
script_root = os.path.dirname(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
|
)
|
||||||
|
sys.path.append(script_root)
|
||||||
|
|
||||||
|
# Importar la función
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Cargar la Configuración
|
||||||
|
|
||||||
|
La función `load_configuration()` busca y carga un archivo llamado `script_config.json` que debe estar en el **mismo directorio** que el script que la ejecuta. Devuelve un diccionario con todo el contenido del JSON.
|
||||||
|
|
||||||
|
### Ejemplo de Uso
|
||||||
|
|
||||||
|
```python
|
||||||
|
def main():
|
||||||
|
# Cargar configuraciones del archivo script_config.json
|
||||||
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Es buena práctica verificar si la configuración se cargó
|
||||||
|
if not configs:
|
||||||
|
print("Error: No se pudo cargar la configuración. Saliendo.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Acceder a los parámetros usando .get() para evitar errores
|
||||||
|
working_directory = configs.get("working_directory", "")
|
||||||
|
level1_config = configs.get("level1", {})
|
||||||
|
level2_config = configs.get("level2", {})
|
||||||
|
level3_config = configs.get("level3", {})
|
||||||
|
|
||||||
|
# Ejemplo de uso de un parámetro específico con valor por defecto
|
||||||
|
scl_output_dir = level2_config.get("scl_output_dir", "scl_output")
|
||||||
|
xref_output_dir = level2_config.get("xref_output_dir", "xref_output")
|
||||||
|
|
||||||
|
print(f"Directorio de trabajo: {working_directory}")
|
||||||
|
print(f"Directorio de salida SCL: {scl_output_dir}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Archivo `script_config.json`
|
||||||
|
|
||||||
|
Este archivo contiene los parámetros de tu script. La estructura interna del JSON, como el uso de `"level1"`, `"level2"`, etc., es una convención para organizar los parámetros. `load_configuration()` simplemente lee el archivo y devuelve su contenido.
|
||||||
|
|
||||||
|
**Ejemplo de `script_config.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"working_directory": "/ruta/al/directorio/de/trabajo",
|
||||||
|
"level1": {
|
||||||
|
"parametro_global_1": "valor1"
|
||||||
|
},
|
||||||
|
"level2": {
|
||||||
|
"scl_output_dir": "scl_output",
|
||||||
|
"xref_output_dir": "xref_output"
|
||||||
|
},
|
||||||
|
"level3": {
|
||||||
|
"parametro_especifico_1": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Manejo de Errores
|
||||||
|
|
||||||
|
`load_configuration()` está diseñada para ser robusta:
|
||||||
|
- Si `script_config.json` no se encuentra, retorna un diccionario vacío `{}`.
|
||||||
|
- Si el JSON es inválido, imprime un error y también retorna `{}`.
|
||||||
|
|
||||||
|
**Siempre** comprueba si el diccionario devuelto está vacío para manejar estos casos de forma segura en tu script.
|
||||||
|
|
||||||
|
## 5. Jerarquía de Archivos de Configuración
|
||||||
|
|
||||||
|
El sistema utiliza un modelo de configuración en cascada para gestionar los parámetros de los scripts. Esta jerarquía permite establecer configuraciones a nivel global, de grupo y de trabajo. Antes de la ejecución, el launcher lee estos archivos, los combina y genera un único `script_config.json` en la carpeta del script. La función `load_configuration()` es la que finalmente lee este archivo consolidado.
|
||||||
|
|
||||||
|
A continuación se describe la finalidad y ubicación de cada archivo clave.
|
||||||
|
|
||||||
|
### Archivos de Valores (Parámetros)
|
||||||
|
|
||||||
|
Contienen los datos y variables que utilizará el script. La configuración se superpone en el siguiente orden: `Nivel 1 < Nivel 2 < Nivel 3`.
|
||||||
|
|
||||||
|
- **`data.json` (Nivel 1 - Global)**
|
||||||
|
- **Ubicación:** `data/data.json`
|
||||||
|
- **Utilidad:** Almacena variables globales disponibles para todos los scripts. Ideal para parámetros generales.
|
||||||
|
- **Acceso:** Sus datos se cargan en la clave `"level1"` del diccionario `configs`.
|
||||||
|
|
||||||
|
- **`script_config.json` (Nivel 2 - Grupo)**
|
||||||
|
- **Ubicación:** En la raíz de cada directorio de grupo (ej: `backend/script_groups/MiGrupo/script_config.json`).
|
||||||
|
- **Utilidad:** Define parámetros compartidos por todos los scripts de un grupo.
|
||||||
|
- **Acceso:** Sus datos se cargan en la clave `"level2"`.
|
||||||
|
|
||||||
|
- **`work_dir.json` (Nivel 3 - Directorio de Trabajo)**
|
||||||
|
- **Ubicación:** Dentro del directorio de trabajo que el script va a procesar.
|
||||||
|
- **Utilidad:** Contiene parámetros para una ejecución específica. Es el nivel más específico y sobrescribe los anteriores.
|
||||||
|
- **Acceso:** Sus datos se cargan en la clave `"level3"`.
|
||||||
|
|
||||||
|
### Archivos de Esquema (Definiciones de Estructura)
|
||||||
|
|
||||||
|
No contienen valores, sino que describen la estructura y tipos de datos que los archivos de valores deben tener. Son usados por el launcher para validación y para generar interfaces de configuración.
|
||||||
|
|
||||||
|
- **`esquema_group.json`**
|
||||||
|
- **Ubicación:** Raíz del directorio del grupo.
|
||||||
|
- **Utilidad:** Define la estructura del `script_config.json` del grupo.
|
||||||
|
|
||||||
|
- **`esquema_work.json`**
|
||||||
|
- **Ubicación:** Raíz del directorio del grupo.
|
||||||
|
- **Utilidad:** Define la estructura del `work_dir.json`.
|
||||||
|
|
||||||
|
## 6. Documentación de Scripts para el Launcher
|
||||||
|
|
||||||
|
El sistema de launcher utiliza archivos JSON para mostrar información sobre los grupos de scripts y scripts individuales en la interfaz web.
|
||||||
|
|
||||||
|
### Archivo description.json (Descripción del Grupo)
|
||||||
|
|
||||||
|
Ubicación: En el directorio raíz del grupo de scripts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Nombre del Grupo",
|
||||||
|
"description": "Descripción del propósito y funcionalidad del grupo",
|
||||||
|
"version": "1.0",
|
||||||
|
"author": "Nombre del Autor"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Archivo scripts_description.json (Descripción de Scripts)
|
||||||
|
|
||||||
|
Ubicación: En el directorio raíz del grupo de scripts.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"nombre_script.py": {
|
||||||
|
"display_name": "Nombre para mostrar en la UI",
|
||||||
|
"short_description": "Descripción breve del script",
|
||||||
|
"long_description": "Descripción detallada con explicación completa de funcionalidad, pasos que ejecuta, y contexto de uso",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"script_interno.py": {
|
||||||
|
"display_name": "Script Interno",
|
||||||
|
"short_description": "Script de uso interno",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Propiedades Importantes
|
||||||
|
|
||||||
|
- **hidden**: `true` oculta el script del launcher (útil para scripts auxiliares)
|
||||||
|
- **display_name**: Nombre amigable que aparece en la interfaz
|
||||||
|
- **short_description**: Se muestra en la lista de scripts
|
||||||
|
- **long_description**: Se muestra al expandir detalles del script
|
||||||
|
|
||||||
|
### Ejemplo Práctico
|
||||||
|
|
||||||
|
Para un grupo "XML Parser to SCL":
|
||||||
|
|
||||||
|
**description.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Siemens-Tia : 03 : Procesador de XML LAD-SCL-AWL",
|
||||||
|
"description": "Scripts que procesan archivos XML exportados de TIA, convirtiendo LAD a SCL",
|
||||||
|
"version": "1.0",
|
||||||
|
"author": "Miguel"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**scripts_description.json:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"x0_main.py": {
|
||||||
|
"display_name": "1: Procesar Exportación XML completa",
|
||||||
|
"short_description": "Conversor principal de LAD/FUP XML a SCL",
|
||||||
|
"long_description": "Script orquestador que procesa todos los archivos XML...",
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"x1_to_json.py": {
|
||||||
|
"display_name": "x1_to_json",
|
||||||
|
"short_description": "Converter XML interno",
|
||||||
|
"long_description": "",
|
||||||
|
"hidden": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Requisitos de Codificación de Salida (stdout)
|
||||||
|
|
||||||
|
Toda la salida estándar (`stdout`) generada por los scripts (por ejemplo, mediante la función `print()`) es capturada en tiempo real y mostrada en el panel de logs del frontend.
|
||||||
|
|
||||||
|
Para garantizar que el texto se muestre correctamente y evitar caracteres corruptos (mojibake), **la salida de los scripts debe tener codificación UTF-8**.
|
||||||
|
|
||||||
|
### Configuración Automática
|
||||||
|
|
||||||
|
El sistema está diseñado para facilitar esto. Al ejecutar un script, el entorno se configura automáticamente para que la salida de Python sea UTF-8. Específicamente, se establece la variable de entorno `PYTHONIOENCODING=utf-8`.
|
||||||
|
|
||||||
|
Gracias a esto, en la mayoría de los casos, **no necesitas hacer nada especial**. Simplemente usa `print()` y la codificación será la correcta.
|
||||||
|
|
||||||
|
### Casos Especiales y Solución de Problemas
|
||||||
|
|
||||||
|
Pueden surgir problemas si tu script lee datos de fuentes externas (como archivos) que tienen una codificación diferente. Si imprimes contenido directamente sin decodificarlo primero, podrías enviar bytes no válidos a la salida.
|
||||||
|
|
||||||
|
La regla es: **decodifica los datos de entrada a un string de Python lo antes posible, y deja que `print()` se encargue de la codificación de salida.**
|
||||||
|
|
||||||
|
**Ejemplo de cómo manejar un archivo con codificación `latin-1`:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# INCORRECTO: Si el archivo no es UTF-8, esto puede enviar bytes inválidos.
|
||||||
|
# El launcher intentará decodificarlos como UTF-8 y podría fallar o mostrar basura.
|
||||||
|
try:
|
||||||
|
with open('mi_archivo_legacy.txt', 'rb') as f:
|
||||||
|
print(f.read())
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Esto probablemente falle o muestre texto corrupto: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# CORRECTO: Leer el archivo especificando su codificación para decodificarlo a un string.
|
||||||
|
# Una vez que es un string de Python, `print` lo manejará correctamente.
|
||||||
|
try:
|
||||||
|
with open('mi_archivo_legacy.txt', 'r', encoding='latin-1') as f:
|
||||||
|
contenido = f.read()
|
||||||
|
# Ahora `contenido` es un string de Python.
|
||||||
|
# print() lo codificará a UTF-8 automáticamente gracias a la configuración del entorno.
|
||||||
|
print(contenido)
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("El archivo 'mi_archivo_legacy.txt' no fue encontrado para el ejemplo.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error al procesar el archivo: {e}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Uso de Servicios Compartidos
|
||||||
|
|
||||||
|
El proyecto ofrece una serie de servicios reutilizables en el directorio `services/` para tareas comunes como la manipulación de archivos Excel, detección de idioma o traducción.
|
||||||
|
|
||||||
|
Para utilizar estos servicios en tu script, asegúrate de que el directorio raíz del proyecto esté en el `sys.path`, como se explica en la sección 1 de esta guía.
|
||||||
|
|
||||||
|
### 8.1 Servicio de Excel (`ExcelService`)
|
||||||
|
|
||||||
|
El `ExcelService` (`services/excel/excel_service.py`) facilita la lectura y escritura de archivos Excel, con manejo de reintentos (por si el archivo está abierto) y opciones de formato.
|
||||||
|
|
||||||
|
**Ejemplo de importación y uso:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Asegúrate de tener el path raíz configurado
|
||||||
|
# ... (código de configuración de sys.path)
|
||||||
|
|
||||||
|
from services.excel.excel_service import ExcelService
|
||||||
|
|
||||||
|
def main():
|
||||||
|
excel_service = ExcelService()
|
||||||
|
|
||||||
|
# Leer un archivo Excel
|
||||||
|
try:
|
||||||
|
df = excel_service.read_excel("mi_archivo_de_entrada.xlsx")
|
||||||
|
print("Datos cargados exitosamente.")
|
||||||
|
|
||||||
|
# ... procesar el DataFrame ...
|
||||||
|
|
||||||
|
# Guardar el DataFrame con formato personalizado
|
||||||
|
format_options = {
|
||||||
|
'freeze_row': 2,
|
||||||
|
'header_color': 'E6E6E6'
|
||||||
|
}
|
||||||
|
|
||||||
|
excel_service.save_excel(
|
||||||
|
df,
|
||||||
|
"mi_archivo_de_salida.xlsx",
|
||||||
|
sheet_name="Resultados",
|
||||||
|
format_options=format_options
|
||||||
|
)
|
||||||
|
print("Archivo guardado con éxito.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ocurrió un error al manejar el archivo Excel: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 Servicios de Lenguaje
|
||||||
|
|
||||||
|
Los servicios de lenguaje (`services/language/`) permiten detectar el idioma de un texto.
|
||||||
|
|
||||||
|
**Ejemplo de importación y uso:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Asegúrate de tener el path raíz configurado
|
||||||
|
# ... (código de configuración de sys.path)
|
||||||
|
|
||||||
|
from services.language.language_factory import LanguageFactory
|
||||||
|
from services.language.language_utils import LanguageUtils
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Crear el servicio de detección de idioma
|
||||||
|
allowed_languages = LanguageUtils.get_available_languages()
|
||||||
|
detector = LanguageFactory.create_service("langid", allowed_languages=allowed_languages)
|
||||||
|
|
||||||
|
# Detectar idioma de un texto
|
||||||
|
text = "Este es un texto de ejemplo en español."
|
||||||
|
lang, confidence = detector.detect_language(text)
|
||||||
|
|
||||||
|
print(f"Texto: '{text}'")
|
||||||
|
print(f"Idioma detectado: {LanguageUtils.get_language_name(lang)} (código: {lang})")
|
||||||
|
print(f"Confianza: {confidence:.2f}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Servicios de LLM (Modelos de Lenguaje Grandes)
|
||||||
|
|
||||||
|
El proyecto integra una fábrica de servicios (`LLMFactory`) para interactuar con diferentes Modelos de Lenguaje Grandes (LLMs). Esto permite a los scripts aprovechar la IA generativa para tareas como el análisis de código, la generación de descripciones semánticas, etc.
|
||||||
|
|
||||||
|
#### 8.3.1 Configuración de API Keys
|
||||||
|
|
||||||
|
La mayoría de los servicios de LLM requieren una clave de API para funcionar. El sistema gestiona esto de forma centralizada a través de variables de entorno.
|
||||||
|
|
||||||
|
1. **Crear el archivo `.env`**: En el **directorio raíz del proyecto**, crea un archivo llamado `.env` (si aún no existe).
|
||||||
|
|
||||||
|
2. **Añadir las API Keys**: Abre el archivo `.env` y añade las claves para los servicios que planeas utilizar. El sistema cargará estas variables automáticamente al inicio.
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Ejemplo de contenido para el archivo .env
|
||||||
|
# (Solo necesitas añadir las claves de los servicios que vayas a usar)
|
||||||
|
|
||||||
|
OPENAI_API_KEY="sk-..."
|
||||||
|
GROQ_API_KEY="gsk_..."
|
||||||
|
CLAUDE_API_KEY="sk-ant-..."
|
||||||
|
GEMINI_API_KEY="AIzaSy..."
|
||||||
|
GROK_API_KEY="TU_API_KEY_DE_GROK"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota**: El servicio `ollama` se ejecuta localmente y no requiere una clave de API.
|
||||||
|
|
||||||
|
#### 8.3.2 Ejemplo de importación y uso
|
||||||
|
|
||||||
|
El siguiente ejemplo muestra cómo un script puede cargar su configuración, inicializar un servicio de LLM y usarlo para generar texto. Este patrón es similar al utilizado en `x3_generate_semantic_descriptions.py`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Asegúrate de tener el path raíz configurado
|
||||||
|
# ... (código de configuración de sys.path)
|
||||||
|
|
||||||
|
from services.llm.llm_factory import LLMFactory
|
||||||
|
from backend.script_utils import load_configuration
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Cargar la configuración del script, que puede incluir qué LLM usar
|
||||||
|
configs = load_configuration()
|
||||||
|
llm_configs = configs.get("llm", {})
|
||||||
|
|
||||||
|
# Obtener el tipo de servicio y otros parámetros del config
|
||||||
|
# Por defecto, usamos 'groq' si no se especifica
|
||||||
|
service_type = llm_configs.get("service", "groq")
|
||||||
|
|
||||||
|
print(f"🤖 Inicializando servicio LLM: {service_type}")
|
||||||
|
|
||||||
|
# Crear una instancia del servicio usando la fábrica
|
||||||
|
# La fábrica se encarga de pasar las API keys desde las variables de entorno
|
||||||
|
llm_service = LLMFactory.create_service(service_type, **llm_configs)
|
||||||
|
|
||||||
|
if not llm_service:
|
||||||
|
print(f"❌ Error: No se pudo crear el servicio LLM '{service_type}'. Abortando.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Usar el servicio para generar texto
|
||||||
|
try:
|
||||||
|
prompt = "Explica la computación cuántica en una sola frase."
|
||||||
|
print(f"Enviando prompt: '{prompt}'")
|
||||||
|
|
||||||
|
description = llm_service.generate_text(prompt)
|
||||||
|
|
||||||
|
print("\nRespuesta del LLM:")
|
||||||
|
print(description)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ocurrió un error al contactar al servicio LLM: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 8.3.3 Servicios Disponibles
|
||||||
|
|
||||||
|
La `LLMFactory` soporta los siguientes tipos de servicio (`service_type`):
|
||||||
|
- `openai`
|
||||||
|
- `groq`
|
||||||
|
- `claude`
|
||||||
|
- `gemini`
|
||||||
|
- `grok`
|
||||||
|
- `ollama` (para ejecución local)
|
|
@ -4,11 +4,12 @@ import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from email.utils import parseaddr, parsedate_to_datetime
|
from email.utils import parseaddr, parsedate_to_datetime
|
||||||
|
|
||||||
|
|
||||||
class MensajeEmail:
|
class MensajeEmail:
|
||||||
def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None):
|
def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None):
|
||||||
self.remitente = self._estandarizar_remitente(remitente)
|
self.remitente = self._estandarizar_remitente(remitente)
|
||||||
self.fecha = self._estandarizar_fecha(fecha)
|
self.fecha = self._estandarizar_fecha(fecha)
|
||||||
self.subject = subject if subject else 'Sin Asunto'
|
self.subject = subject if subject else "Sin Asunto"
|
||||||
self.contenido = self._limpiar_contenido(contenido)
|
self.contenido = self._limpiar_contenido(contenido)
|
||||||
self.adjuntos = adjuntos if adjuntos else []
|
self.adjuntos = adjuntos if adjuntos else []
|
||||||
self.hash = self._generar_hash()
|
self.hash = self._generar_hash()
|
||||||
|
@ -21,8 +22,8 @@ class MensajeEmail:
|
||||||
if not subject:
|
if not subject:
|
||||||
return "Sin-Asunto"
|
return "Sin-Asunto"
|
||||||
# Eliminar caracteres especiales y reemplazar espacios con guiones
|
# Eliminar caracteres especiales y reemplazar espacios con guiones
|
||||||
formatted = re.sub(r'[^\w\s-]', '', subject)
|
formatted = re.sub(r"[^\w\s-]", "", subject)
|
||||||
formatted = re.sub(r'\s+', '-', formatted.strip())
|
formatted = re.sub(r"\s+", "-", formatted.strip())
|
||||||
return formatted
|
return formatted
|
||||||
|
|
||||||
def _limpiar_contenido(self, contenido):
|
def _limpiar_contenido(self, contenido):
|
||||||
|
@ -30,31 +31,33 @@ class MensajeEmail:
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
# Eliminar líneas de metadatos
|
# Eliminar líneas de metadatos
|
||||||
lines = contenido.split('\n')
|
lines = contenido.split("\n")
|
||||||
cleaned_lines = []
|
cleaned_lines = []
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
# Skip metadata lines
|
# Skip metadata lines
|
||||||
if line.strip().startswith(('Da: ', 'Inviato: ', 'A: ', 'From: ', 'Sent: ', 'To: ')) or line.strip().startswith('Oggetto: '):
|
if line.strip().startswith(
|
||||||
|
("Da: ", "Inviato: ", "A: ", "From: ", "Sent: ", "To: ")
|
||||||
|
) or line.strip().startswith("Oggetto: "):
|
||||||
continue
|
continue
|
||||||
# Limpiar espacios múltiples dentro de cada línea, pero mantener la línea completa
|
# Limpiar espacios múltiples dentro de cada línea, pero mantener la línea completa
|
||||||
cleaned_line = re.sub(r' +', ' ', line)
|
cleaned_line = re.sub(r" +", " ", line)
|
||||||
cleaned_lines.append(cleaned_line)
|
cleaned_lines.append(cleaned_line)
|
||||||
|
|
||||||
# Unir las líneas preservando los saltos de línea
|
# Unir las líneas preservando los saltos de línea
|
||||||
text = '\n'.join(cleaned_lines)
|
text = "\n".join(cleaned_lines)
|
||||||
|
|
||||||
# Limpiar la combinación específica de CRLF+NBSP+CRLF
|
# Limpiar la combinación específica de CRLF+NBSP+CRLF
|
||||||
text = re.sub(r'\r?\n\xa0\r?\n', '\n', text)
|
text = re.sub(r"\r?\n\xa0\r?\n", "\n", text)
|
||||||
|
|
||||||
# Reemplazar CRLF por LF
|
# Reemplazar CRLF por LF
|
||||||
text = text.replace('\r\n', '\n')
|
text = text.replace("\r\n", "\n")
|
||||||
|
|
||||||
# Reemplazar CR por LF
|
# Reemplazar CR por LF
|
||||||
text = text.replace('\r', '\n')
|
text = text.replace("\r", "\n")
|
||||||
|
|
||||||
# Reemplazar 3 o más saltos de línea por dos
|
# Reemplazar 3 o más saltos de línea por dos
|
||||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
text = re.sub(r"\n{3,}", "\n\n", text)
|
||||||
|
|
||||||
# Eliminar espacios al inicio y final del texto completo
|
# Eliminar espacios al inicio y final del texto completo
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
@ -67,7 +70,7 @@ class MensajeEmail:
|
||||||
subject_line = f"### {self.subject if self.subject else 'Sin Asunto'}\n\n"
|
subject_line = f"### {self.subject if self.subject else 'Sin Asunto'}\n\n"
|
||||||
|
|
||||||
# Fecha en formato legible
|
# Fecha en formato legible
|
||||||
fecha_formato = self.fecha.strftime('%d-%m-%Y')
|
fecha_formato = self.fecha.strftime("%d-%m-%Y")
|
||||||
fecha_line = f"- {fecha_formato}\n\n"
|
fecha_line = f"- {fecha_formato}\n\n"
|
||||||
|
|
||||||
# Contenido del mensaje
|
# Contenido del mensaje
|
||||||
|
@ -86,28 +89,28 @@ class MensajeEmail:
|
||||||
"""
|
"""
|
||||||
Genera una entrada de lista para el índice
|
Genera una entrada de lista para el índice
|
||||||
"""
|
"""
|
||||||
fecha_formato = self.fecha.strftime('%d-%m-%Y')
|
fecha_formato = self.fecha.strftime("%d-%m-%Y")
|
||||||
subject_link = self._formatear_subject_para_link(self.subject)
|
subject_link = self._formatear_subject_para_link(self.subject)
|
||||||
return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]"
|
return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]"
|
||||||
|
|
||||||
def _estandarizar_remitente(self, remitente):
|
def _estandarizar_remitente(self, remitente):
|
||||||
if 'Da:' in remitente:
|
if "Da:" in remitente:
|
||||||
remitente = remitente.split('Da:')[1].split('Inviato:')[0]
|
remitente = remitente.split("Da:")[1].split("Inviato:")[0]
|
||||||
elif 'From:' in remitente:
|
elif "From:" in remitente:
|
||||||
remitente = remitente.split('From:')[1].split('Sent:')[0]
|
remitente = remitente.split("From:")[1].split("Sent:")[0]
|
||||||
|
|
||||||
nombre, email = parseaddr(remitente)
|
nombre, email = parseaddr(remitente)
|
||||||
if not nombre and email:
|
if not nombre and email:
|
||||||
nombre = email.split('@')[0]
|
nombre = email.split("@")[0]
|
||||||
elif not nombre and not email:
|
elif not nombre and not email:
|
||||||
nombre_match = re.search(r'([A-Za-z\s]+)\s*<', remitente)
|
nombre_match = re.search(r"([A-Za-z\s]+)\s*<", remitente)
|
||||||
if nombre_match:
|
if nombre_match:
|
||||||
nombre = nombre_match.group(1)
|
nombre = nombre_match.group(1)
|
||||||
else:
|
else:
|
||||||
return "Remitente Desconocido"
|
return "Remitente Desconocido"
|
||||||
|
|
||||||
nombre = re.sub(r'[<>:"/\\|?*]', '', nombre.strip())
|
nombre = re.sub(r'[<>:"/\\|?*]', "", nombre.strip())
|
||||||
nombre = nombre.encode('ascii', 'ignore').decode('ascii')
|
nombre = nombre.encode("ascii", "ignore").decode("ascii")
|
||||||
return nombre
|
return nombre
|
||||||
|
|
||||||
def _estandarizar_fecha(self, fecha):
|
def _estandarizar_fecha(self, fecha):
|
||||||
|
@ -125,21 +128,47 @@ class MensajeEmail:
|
||||||
"""
|
"""
|
||||||
# Limpiar y normalizar el contenido para el hash
|
# Limpiar y normalizar el contenido para el hash
|
||||||
# Para el hash, sí normalizamos completamente los espacios
|
# Para el hash, sí normalizamos completamente los espacios
|
||||||
contenido_hash = re.sub(r'\s+', ' ', self.contenido).strip()
|
contenido_hash = re.sub(r"\s+", " ", self.contenido).strip()
|
||||||
|
|
||||||
# Normalizar el subject
|
# Normalizar el subject
|
||||||
subject_normalizado = re.sub(r'\s+', ' ', self.subject if self.subject else '').strip()
|
subject_normalizado = re.sub(
|
||||||
|
r"\s+", " ", self.subject if self.subject else ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
# Crear una cadena con los elementos clave del mensaje
|
# Crear una cadena con los elementos clave del mensaje
|
||||||
elementos_hash = [
|
elementos_hash = [
|
||||||
self.remitente.strip(),
|
self.remitente.strip(),
|
||||||
self.fecha.strftime('%Y%m%d%H%M'), # Solo hasta minutos para permitir pequeñas variaciones
|
self.fecha.strftime(
|
||||||
|
"%Y%m%d%H%M"
|
||||||
|
), # Solo hasta minutos para permitir pequeñas variaciones
|
||||||
subject_normalizado,
|
subject_normalizado,
|
||||||
contenido_hash[:500] # Usar solo los primeros 500 caracteres del contenido normalizado
|
contenido_hash[
|
||||||
|
:500
|
||||||
|
], # Usar solo los primeros 500 caracteres del contenido normalizado
|
||||||
]
|
]
|
||||||
|
|
||||||
# Unir todos los elementos con un separador único
|
# Unir todos los elementos con un separador único
|
||||||
texto_hash = '|'.join(elementos_hash)
|
texto_hash = "|".join(elementos_hash)
|
||||||
|
|
||||||
|
# Mostrar información de debug para el hash (solo si está habilitado)
|
||||||
|
if hasattr(self, "_debug_hash") and self._debug_hash:
|
||||||
|
print(f" 🔍 Debug Hash:")
|
||||||
|
print(f" - Remitente: '{self.remitente.strip()}'")
|
||||||
|
print(f" - Fecha: '{self.fecha.strftime('%Y%m%d%H%M')}'")
|
||||||
|
print(f" - Subject: '{subject_normalizado}'")
|
||||||
|
print(f" - Contenido (500 chars): '{contenido_hash[:500]}'")
|
||||||
|
print(f" - Texto completo hash: '{texto_hash[:100]}...'")
|
||||||
|
|
||||||
# Generar el hash
|
# Generar el hash
|
||||||
return hashlib.md5(texto_hash.encode()).hexdigest()
|
hash_resultado = hashlib.md5(texto_hash.encode()).hexdigest()
|
||||||
|
|
||||||
|
return hash_resultado
|
||||||
|
|
||||||
|
def debug_hash_info(self):
|
||||||
|
"""
|
||||||
|
Muestra información detallada sobre cómo se genera el hash de este mensaje
|
||||||
|
"""
|
||||||
|
self._debug_hash = True
|
||||||
|
hash_result = self._generar_hash()
|
||||||
|
delattr(self, "_debug_hash")
|
||||||
|
return hash_result
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"cronologia_file": "cronologia.md"
|
"cronologia_file": "cronologia.md"
|
||||||
},
|
},
|
||||||
"level3": {
|
"level3": {
|
||||||
"output_directory": "C:/Users/migue/OneDrive/Miguel/Obsidean/Trabajo/VM/04-SIDEL/14 - E5.007172 - Modifica O&U - SAE340"
|
"output_directory": "C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\03-VM\\45 - HENKEL - VM Auto Changeover"
|
||||||
},
|
},
|
||||||
"working_directory": "C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email"
|
"working_directory": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\Entregado por VM\\01 - 26-07-2025 Max - Emails"
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class PatternType(Enum):
|
class PatternType(Enum):
|
||||||
REGEX = "regex"
|
REGEX = "regex"
|
||||||
STRING = "string"
|
STRING = "string"
|
||||||
|
@ -11,6 +12,7 @@ class PatternType(Enum):
|
||||||
RIGHT = "right"
|
RIGHT = "right"
|
||||||
SUBSTRING = "substring"
|
SUBSTRING = "substring"
|
||||||
|
|
||||||
|
|
||||||
class BeautifyProcessor:
|
class BeautifyProcessor:
|
||||||
def __init__(self, rules_file):
|
def __init__(self, rules_file):
|
||||||
self.rules_by_priority = self._load_rules(rules_file)
|
self.rules_by_priority = self._load_rules(rules_file)
|
||||||
|
@ -19,19 +21,21 @@ class BeautifyProcessor:
|
||||||
rules_by_priority = defaultdict(list)
|
rules_by_priority = defaultdict(list)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(rules_file, 'r', encoding='utf-8') as f:
|
with open(rules_file, "r", encoding="utf-8") as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
|
|
||||||
if not isinstance(data, dict) or 'rules' not in data:
|
if not isinstance(data, dict) or "rules" not in data:
|
||||||
raise ValueError("El archivo JSON debe contener un objeto con una clave 'rules'")
|
raise ValueError(
|
||||||
|
"El archivo JSON debe contener un objeto con una clave 'rules'"
|
||||||
|
)
|
||||||
|
|
||||||
for rule in data['rules']:
|
for rule in data["rules"]:
|
||||||
try:
|
try:
|
||||||
pattern = rule['pattern']
|
pattern = rule["pattern"]
|
||||||
replacement = rule['replacement']
|
replacement = rule["replacement"]
|
||||||
action = rule['action']
|
action = rule["action"]
|
||||||
pattern_type = PatternType(rule.get('type', 'string'))
|
pattern_type = PatternType(rule.get("type", "string"))
|
||||||
priority = int(rule.get('priority', 999))
|
priority = int(rule.get("priority", 999))
|
||||||
|
|
||||||
# Para remove_block, convertir el patrón con ..... a una regex
|
# Para remove_block, convertir el patrón con ..... a una regex
|
||||||
if action == "remove_block":
|
if action == "remove_block":
|
||||||
|
@ -40,7 +44,9 @@ class BeautifyProcessor:
|
||||||
elif pattern_type == PatternType.REGEX:
|
elif pattern_type == PatternType.REGEX:
|
||||||
pattern = re.compile(pattern)
|
pattern = re.compile(pattern)
|
||||||
|
|
||||||
rules_by_priority[priority].append((pattern, replacement, action, pattern_type))
|
rules_by_priority[priority].append(
|
||||||
|
(pattern, replacement, action, pattern_type)
|
||||||
|
)
|
||||||
|
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
print(f"Error en regla: falta campo requerido {e}")
|
print(f"Error en regla: falta campo requerido {e}")
|
||||||
|
@ -74,7 +80,7 @@ class BeautifyProcessor:
|
||||||
# Restaurar el marcador con el patrón .*?
|
# Restaurar el marcador con el patrón .*?
|
||||||
pattern = pattern.replace(marker, ".*?")
|
pattern = pattern.replace(marker, ".*?")
|
||||||
|
|
||||||
return re.compile(f'(?s){pattern}')
|
return re.compile(f"(?s){pattern}")
|
||||||
|
|
||||||
def _process_remove_block(self, text, pattern):
|
def _process_remove_block(self, text, pattern):
|
||||||
result = text
|
result = text
|
||||||
|
@ -83,22 +89,31 @@ class BeautifyProcessor:
|
||||||
for match in reversed(matches):
|
for match in reversed(matches):
|
||||||
start, end = match.span()
|
start, end = match.span()
|
||||||
|
|
||||||
line_start = result.rfind('\n', 0, start) + 1
|
line_start = result.rfind("\n", 0, start) + 1
|
||||||
if line_start == 0:
|
if line_start == 0:
|
||||||
line_start = 0
|
line_start = 0
|
||||||
|
|
||||||
line_end = result.find('\n', end)
|
line_end = result.find("\n", end)
|
||||||
if line_end == -1:
|
if line_end == -1:
|
||||||
line_end = len(result)
|
line_end = len(result)
|
||||||
else:
|
else:
|
||||||
line_end += 1
|
line_end += 1
|
||||||
|
|
||||||
while line_start > 0 and result[line_start-1:line_start] == '\n' and \
|
while (
|
||||||
(line_start == 1 or result[line_start-2:line_start-1] == '\n'):
|
line_start > 0
|
||||||
|
and result[line_start - 1 : line_start] == "\n"
|
||||||
|
and (line_start == 1 or result[line_start - 2 : line_start - 1] == "\n")
|
||||||
|
):
|
||||||
line_start -= 1
|
line_start -= 1
|
||||||
|
|
||||||
while line_end < len(result) and result[line_end-1:line_end] == '\n' and \
|
while (
|
||||||
(line_end == len(result)-1 or result[line_end:line_end+1] == '\n'):
|
line_end < len(result)
|
||||||
|
and result[line_end - 1 : line_end] == "\n"
|
||||||
|
and (
|
||||||
|
line_end == len(result) - 1
|
||||||
|
or result[line_end : line_end + 1] == "\n"
|
||||||
|
)
|
||||||
|
):
|
||||||
line_end += 1
|
line_end += 1
|
||||||
|
|
||||||
result = result[:line_start] + result[line_end:]
|
result = result[:line_start] + result[line_end:]
|
||||||
|
@ -134,16 +149,20 @@ class BeautifyProcessor:
|
||||||
result_lines.append(line.replace(pattern, replacement, 1))
|
result_lines.append(line.replace(pattern, replacement, 1))
|
||||||
else:
|
else:
|
||||||
result_lines.append(line)
|
result_lines.append(line)
|
||||||
return '\n'.join(result_lines)
|
return "\n".join(result_lines)
|
||||||
elif pattern_type == PatternType.RIGHT:
|
elif pattern_type == PatternType.RIGHT:
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
result_lines = []
|
result_lines = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if line.strip().endswith(pattern):
|
if line.strip().endswith(pattern):
|
||||||
result_lines.append(line[:line.rindex(pattern)] + replacement + line[line.rindex(pattern) + len(pattern):])
|
result_lines.append(
|
||||||
|
line[: line.rindex(pattern)]
|
||||||
|
+ replacement
|
||||||
|
+ line[line.rindex(pattern) + len(pattern) :]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
result_lines.append(line)
|
result_lines.append(line)
|
||||||
return '\n'.join(result_lines)
|
return "\n".join(result_lines)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def process_text(self, text):
|
def process_text(self, text):
|
||||||
|
@ -153,18 +172,23 @@ class BeautifyProcessor:
|
||||||
result = text
|
result = text
|
||||||
for priority in sorted(self.rules_by_priority.keys()):
|
for priority in sorted(self.rules_by_priority.keys()):
|
||||||
rules = self.rules_by_priority[priority]
|
rules = self.rules_by_priority[priority]
|
||||||
print(f"Aplicando reglas de prioridad {priority}")
|
|
||||||
|
|
||||||
for pattern, replacement, action, pattern_type in rules:
|
for pattern, replacement, action, pattern_type in rules:
|
||||||
try:
|
try:
|
||||||
if action == "remove_block":
|
if action == "remove_block":
|
||||||
result = self._process_remove_block(result, pattern)
|
result = self._process_remove_block(result, pattern)
|
||||||
elif action == "replace":
|
elif action == "replace":
|
||||||
result = self._apply_replace(result, pattern, replacement, pattern_type)
|
result = self._apply_replace(
|
||||||
|
result, pattern, replacement, pattern_type
|
||||||
|
)
|
||||||
elif action == "remove_line":
|
elif action == "remove_line":
|
||||||
result = self._process_remove_line(result, pattern, pattern_type)
|
result = self._process_remove_line(
|
||||||
|
result, pattern, pattern_type
|
||||||
|
)
|
||||||
elif action in ["add_before", "add_after"]:
|
elif action in ["add_before", "add_after"]:
|
||||||
result = self._process_line_additions(result, pattern, replacement, action, pattern_type)
|
result = self._process_line_additions(
|
||||||
|
result, pattern, replacement, action, pattern_type
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error aplicando regla {pattern}: {e}")
|
print(f"Error aplicando regla {pattern}: {e}")
|
||||||
continue
|
continue
|
||||||
|
@ -173,13 +197,13 @@ class BeautifyProcessor:
|
||||||
|
|
||||||
def process_file(self, input_file, output_file=None):
|
def process_file(self, input_file, output_file=None):
|
||||||
try:
|
try:
|
||||||
with open(input_file, 'r', encoding='utf-8') as f:
|
with open(input_file, "r", encoding="utf-8") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
processed_content = self.process_text(content)
|
processed_content = self.process_text(content)
|
||||||
|
|
||||||
output = output_file or input_file
|
output = output_file or input_file
|
||||||
with open(output, 'w', encoding='utf-8') as f:
|
with open(output, "w", encoding="utf-8") as f:
|
||||||
f.write(processed_content)
|
f.write(processed_content)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -205,7 +229,7 @@ class BeautifyProcessor:
|
||||||
result_lines.append(line)
|
result_lines.append(line)
|
||||||
skip_next_empty = False
|
skip_next_empty = False
|
||||||
|
|
||||||
return '\n'.join(result_lines)
|
return "\n".join(result_lines)
|
||||||
|
|
||||||
def _process_line_additions(self, text, pattern, replacement, action, pattern_type):
|
def _process_line_additions(self, text, pattern, replacement, action, pattern_type):
|
||||||
lines = text.splitlines()
|
lines = text.splitlines()
|
||||||
|
@ -222,4 +246,4 @@ class BeautifyProcessor:
|
||||||
else:
|
else:
|
||||||
result_lines.append(line)
|
result_lines.append(line)
|
||||||
|
|
||||||
return '\n'.join(result_lines)
|
return "\n".join(result_lines)
|
||||||
|
|
|
@ -12,6 +12,7 @@ from utils.attachment_handler import guardar_adjunto
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
def _get_payload_safely(parte):
|
def _get_payload_safely(parte):
|
||||||
"""
|
"""
|
||||||
Obtiene el payload de una parte del email de forma segura
|
Obtiene el payload de una parte del email de forma segura
|
||||||
|
@ -22,44 +23,57 @@ def _get_payload_safely(parte):
|
||||||
payload = parte.get_payload(decode=True)
|
payload = parte.get_payload(decode=True)
|
||||||
if payload is None:
|
if payload is None:
|
||||||
return None
|
return None
|
||||||
charset = parte.get_content_charset() or 'utf-8'
|
charset = parte.get_content_charset() or "utf-8"
|
||||||
return payload.decode(charset, errors='ignore')
|
return payload.decode(charset, errors="ignore")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error getting payload: {str(e)}")
|
print(f"Error getting payload: {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _extract_subject_from_text(text):
|
def _extract_subject_from_text(text):
|
||||||
"""
|
"""
|
||||||
Extrae el asunto de un texto dados diferentes formatos de cabecera
|
Extrae el asunto de un texto dados diferentes formatos de cabecera
|
||||||
"""
|
"""
|
||||||
subject_headers = {
|
subject_headers = {
|
||||||
'Oggetto: ': 9, # Italian
|
"Oggetto: ": 9, # Italian
|
||||||
'Subject: ': 9, # English
|
"Subject: ": 9, # English
|
||||||
'Asunto: ': 8, # Spanish
|
"Asunto: ": 8, # Spanish
|
||||||
'Sujet: ': 7, # French
|
"Sujet: ": 7, # French
|
||||||
'Betreff: ': 9 # German
|
"Betreff: ": 9, # German
|
||||||
}
|
}
|
||||||
|
|
||||||
for line in text.split('\n'):
|
for line in text.split("\n"):
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
for header, offset in subject_headers.items():
|
for header, offset in subject_headers.items():
|
||||||
if line.startswith(header):
|
if line.startswith(header):
|
||||||
return line[offset:].strip()
|
return line[offset:].strip()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _should_skip_line(line):
|
def _should_skip_line(line):
|
||||||
"""
|
"""
|
||||||
Determina si una línea debe ser omitida por ser una cabecera de email
|
Determina si una línea debe ser omitida por ser una cabecera de email
|
||||||
"""
|
"""
|
||||||
headers_to_skip = [
|
headers_to_skip = [
|
||||||
'Da: ', 'Inviato: ', 'A: ', # Italian
|
"Da: ",
|
||||||
'From: ', 'Sent: ', 'To: ', # English
|
"Inviato: ",
|
||||||
'De: ', 'Enviado: ', 'Para: ', # Spanish
|
"A: ", # Italian
|
||||||
'Von: ', 'Gesendet: ', 'An: ', # German
|
"From: ",
|
||||||
'De : ', 'Envoyé : ', 'À : ' # French
|
"Sent: ",
|
||||||
|
"To: ", # English
|
||||||
|
"De: ",
|
||||||
|
"Enviado: ",
|
||||||
|
"Para: ", # Spanish
|
||||||
|
"Von: ",
|
||||||
|
"Gesendet: ",
|
||||||
|
"An: ", # German
|
||||||
|
"De : ",
|
||||||
|
"Envoyé : ",
|
||||||
|
"À : ", # French
|
||||||
]
|
]
|
||||||
return any(line.strip().startswith(header) for header in headers_to_skip)
|
return any(line.strip().startswith(header) for header in headers_to_skip)
|
||||||
|
|
||||||
|
|
||||||
def _html_a_markdown(html):
|
def _html_a_markdown(html):
|
||||||
"""
|
"""
|
||||||
Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente
|
Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente
|
||||||
|
@ -69,16 +83,16 @@ def _html_a_markdown(html):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Limpieza básica
|
# Limpieza básica
|
||||||
html = html.replace('\xa0', ' ') # NBSP a espacio normal
|
html = html.replace("\xa0", " ") # NBSP a espacio normal
|
||||||
html = html.replace('\r\n', '\n') # CRLF a LF
|
html = html.replace("\r\n", "\n") # CRLF a LF
|
||||||
html = html.replace('\r', '\n') # CR a LF
|
html = html.replace("\r", "\n") # CR a LF
|
||||||
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
# Procesar tablas
|
# Procesar tablas
|
||||||
for table in soup.find_all('table'):
|
for table in soup.find_all("table"):
|
||||||
try:
|
try:
|
||||||
rows = table.find_all('tr')
|
rows = table.find_all("tr")
|
||||||
if not rows:
|
if not rows:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -90,7 +104,7 @@ def _html_a_markdown(html):
|
||||||
row_idx = 0
|
row_idx = 0
|
||||||
while row_idx < len(rows):
|
while row_idx < len(rows):
|
||||||
row = rows[row_idx]
|
row = rows[row_idx]
|
||||||
cells = row.find_all(['th', 'td'])
|
cells = row.find_all(["th", "td"])
|
||||||
if not cells:
|
if not cells:
|
||||||
row_idx += 1
|
row_idx += 1
|
||||||
continue
|
continue
|
||||||
|
@ -102,17 +116,22 @@ def _html_a_markdown(html):
|
||||||
col_idx = 0
|
col_idx = 0
|
||||||
for cell in cells:
|
for cell in cells:
|
||||||
# Encontrar la siguiente columna disponible
|
# Encontrar la siguiente columna disponible
|
||||||
while col_idx < len(table_matrix[row_idx]) and table_matrix[row_idx][col_idx] is not None:
|
while (
|
||||||
|
col_idx < len(table_matrix[row_idx])
|
||||||
|
and table_matrix[row_idx][col_idx] is not None
|
||||||
|
):
|
||||||
col_idx += 1
|
col_idx += 1
|
||||||
|
|
||||||
# Obtener rowspan y colspan
|
# Obtener rowspan y colspan
|
||||||
rowspan = int(cell.get('rowspan', 1))
|
rowspan = int(cell.get("rowspan", 1))
|
||||||
colspan = int(cell.get('colspan', 1))
|
colspan = int(cell.get("colspan", 1))
|
||||||
|
|
||||||
# Procesar el texto de la celda reemplazando saltos de línea por <br>
|
# Procesar el texto de la celda reemplazando saltos de línea por <br>
|
||||||
cell_text = cell.get_text().strip()
|
cell_text = cell.get_text().strip()
|
||||||
cell_text = cell_text.replace('\n', '<br>')
|
cell_text = cell_text.replace("\n", "<br>")
|
||||||
cell_text = re.sub(r'\s*<br>\s*<br>\s*', '<br>', cell_text) # Eliminar <br> múltiples
|
cell_text = re.sub(
|
||||||
|
r"\s*<br>\s*<br>\s*", "<br>", cell_text
|
||||||
|
) # Eliminar <br> múltiples
|
||||||
cell_text = cell_text.strip()
|
cell_text = cell_text.strip()
|
||||||
|
|
||||||
# Rellenar la matriz con el texto y None para las celdas combinadas
|
# Rellenar la matriz con el texto y None para las celdas combinadas
|
||||||
|
@ -122,14 +141,16 @@ def _html_a_markdown(html):
|
||||||
while len(table_matrix) <= current_row:
|
while len(table_matrix) <= current_row:
|
||||||
table_matrix.append([])
|
table_matrix.append([])
|
||||||
# Expandir fila si es necesario
|
# Expandir fila si es necesario
|
||||||
while len(table_matrix[current_row]) <= col_idx + colspan - 1:
|
while (
|
||||||
|
len(table_matrix[current_row]) <= col_idx + colspan - 1
|
||||||
|
):
|
||||||
table_matrix[current_row].append(None)
|
table_matrix[current_row].append(None)
|
||||||
|
|
||||||
for c in range(colspan):
|
for c in range(colspan):
|
||||||
if r == 0 and c == 0:
|
if r == 0 and c == 0:
|
||||||
table_matrix[current_row][col_idx + c] = cell_text
|
table_matrix[current_row][col_idx + c] = cell_text
|
||||||
else:
|
else:
|
||||||
table_matrix[current_row][col_idx + c] = ''
|
table_matrix[current_row][col_idx + c] = ""
|
||||||
|
|
||||||
col_idx += colspan
|
col_idx += colspan
|
||||||
|
|
||||||
|
@ -139,51 +160,55 @@ def _html_a_markdown(html):
|
||||||
# Asegurar que todas las filas tengan el mismo número de columnas
|
# Asegurar que todas las filas tengan el mismo número de columnas
|
||||||
for row in table_matrix:
|
for row in table_matrix:
|
||||||
while len(row) < max_cols:
|
while len(row) < max_cols:
|
||||||
row.append('')
|
row.append("")
|
||||||
|
|
||||||
# Calcular anchos máximos por columna
|
# Calcular anchos máximos por columna
|
||||||
col_widths = [0] * max_cols
|
col_widths = [0] * max_cols
|
||||||
for row in table_matrix:
|
for row in table_matrix:
|
||||||
for col_idx, cell in enumerate(row):
|
for col_idx, cell in enumerate(row):
|
||||||
if cell is not None:
|
if cell is not None:
|
||||||
col_widths[col_idx] = max(col_widths[col_idx], len(str(cell)))
|
col_widths[col_idx] = max(
|
||||||
|
col_widths[col_idx], len(str(cell))
|
||||||
|
)
|
||||||
|
|
||||||
# Generar tabla Markdown
|
# Generar tabla Markdown
|
||||||
markdown_table = []
|
markdown_table = []
|
||||||
|
|
||||||
# Cabecera
|
# Cabecera
|
||||||
if table_matrix:
|
if table_matrix:
|
||||||
header = '|'
|
header = "|"
|
||||||
for col_idx, width in enumerate(col_widths):
|
for col_idx, width in enumerate(col_widths):
|
||||||
cell = str(table_matrix[0][col_idx] or '')
|
cell = str(table_matrix[0][col_idx] or "")
|
||||||
header += f' {cell.ljust(width)} |'
|
header += f" {cell.ljust(width)} |"
|
||||||
markdown_table.append(header)
|
markdown_table.append(header)
|
||||||
|
|
||||||
# Separador
|
# Separador
|
||||||
separator = '|'
|
separator = "|"
|
||||||
for width in col_widths:
|
for width in col_widths:
|
||||||
separator += '-' * (width + 2) + '|'
|
separator += "-" * (width + 2) + "|"
|
||||||
markdown_table.append(separator)
|
markdown_table.append(separator)
|
||||||
|
|
||||||
# Contenido
|
# Contenido
|
||||||
for row_idx in range(1, len(table_matrix)):
|
for row_idx in range(1, len(table_matrix)):
|
||||||
row_text = '|'
|
row_text = "|"
|
||||||
for col_idx, width in enumerate(col_widths):
|
for col_idx, width in enumerate(col_widths):
|
||||||
cell = str(table_matrix[row_idx][col_idx] or '')
|
cell = str(table_matrix[row_idx][col_idx] or "")
|
||||||
row_text += f' {cell.ljust(width)} |'
|
row_text += f" {cell.ljust(width)} |"
|
||||||
markdown_table.append(row_text)
|
markdown_table.append(row_text)
|
||||||
|
|
||||||
# Reemplazar la tabla HTML con la versión Markdown
|
# Reemplazar la tabla HTML con la versión Markdown
|
||||||
if markdown_table:
|
if markdown_table:
|
||||||
table.replace_with(soup.new_string('\n' + '\n'.join(markdown_table) + '\n'))
|
table.replace_with(
|
||||||
|
soup.new_string("\n" + "\n".join(markdown_table) + "\n")
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error procesando tabla: {str(e)}")
|
print(f"Error procesando tabla: {str(e)}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Procesar saltos de línea
|
# Procesar saltos de línea
|
||||||
for br in soup.find_all('br'):
|
for br in soup.find_all("br"):
|
||||||
br.replace_with('\n')
|
br.replace_with("\n")
|
||||||
|
|
||||||
# Obtener texto limpio
|
# Obtener texto limpio
|
||||||
text = soup.get_text()
|
text = soup.get_text()
|
||||||
|
@ -192,20 +217,21 @@ def _html_a_markdown(html):
|
||||||
cleaned_lines = []
|
cleaned_lines = []
|
||||||
subject = None
|
subject = None
|
||||||
|
|
||||||
for line in text.split('\n'):
|
for line in text.split("\n"):
|
||||||
if not subject:
|
if not subject:
|
||||||
subject = _extract_subject_from_text(line)
|
subject = _extract_subject_from_text(line)
|
||||||
|
|
||||||
if not _should_skip_line(line):
|
if not _should_skip_line(line):
|
||||||
cleaned_lines.append(line)
|
cleaned_lines.append(line)
|
||||||
|
|
||||||
final_text = '\n'.join(cleaned_lines).strip()
|
final_text = "\n".join(cleaned_lines).strip()
|
||||||
return (subject, final_text)
|
return (subject, final_text)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error en html_a_markdown: {str(e)}")
|
print(f"Error en html_a_markdown: {str(e)}")
|
||||||
return (None, html if html else "")
|
return (None, html if html else "")
|
||||||
|
|
||||||
|
|
||||||
def _procesar_email_adjunto(parte, dir_adjuntos):
|
def _procesar_email_adjunto(parte, dir_adjuntos):
|
||||||
"""
|
"""
|
||||||
Procesa un email que viene como adjunto dentro de otro email.
|
Procesa un email que viene como adjunto dentro de otro email.
|
||||||
|
@ -237,18 +263,24 @@ def _procesar_email_adjunto(parte, dir_adjuntos):
|
||||||
print(f"Error procesando email adjunto: {str(e)}")
|
print(f"Error procesando email adjunto: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def procesar_eml(ruta_archivo, dir_adjuntos):
|
def procesar_eml(ruta_archivo, dir_adjuntos):
|
||||||
"""
|
"""
|
||||||
Punto de entrada principal para procesar archivos .eml
|
Punto de entrada principal para procesar archivos .eml
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
with open(ruta_archivo, 'rb') as eml:
|
print(f" 📧 Abriendo archivo: {ruta_archivo}")
|
||||||
|
with open(ruta_archivo, "rb") as eml:
|
||||||
mensaje = BytesParser(policy=policy.default).parse(eml)
|
mensaje = BytesParser(policy=policy.default).parse(eml)
|
||||||
return procesar_eml_interno(mensaje, dir_adjuntos)
|
|
||||||
|
mensajes = procesar_eml_interno(mensaje, dir_adjuntos)
|
||||||
|
print(f" 📧 Procesamiento completado: {len(mensajes)} mensajes extraídos")
|
||||||
|
return mensajes
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error al abrir el archivo {ruta_archivo}: {str(e)}")
|
print(f"❌ Error al abrir el archivo {ruta_archivo}: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def procesar_eml_interno(mensaje, dir_adjuntos):
|
def procesar_eml_interno(mensaje, dir_adjuntos):
|
||||||
"""
|
"""
|
||||||
Procesa un mensaje de email, ya sea desde archivo o adjunto
|
Procesa un mensaje de email, ya sea desde archivo o adjunto
|
||||||
|
@ -256,12 +288,12 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
|
||||||
mensajes = []
|
mensajes = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
remitente = mensaje.get('from', '')
|
remitente = mensaje.get("from", "")
|
||||||
fecha_str = mensaje.get('date', '')
|
fecha_str = mensaje.get("date", "")
|
||||||
fecha = _parsear_fecha(fecha_str)
|
fecha = _parsear_fecha(fecha_str)
|
||||||
|
|
||||||
# Get subject from email headers first
|
# Get subject from email headers first
|
||||||
subject = mensaje.get('subject', '')
|
subject = mensaje.get("subject", "")
|
||||||
if subject:
|
if subject:
|
||||||
# Try to decode if it's encoded
|
# Try to decode if it's encoded
|
||||||
subject = str(email.header.make_header(email.header.decode_header(subject)))
|
subject = str(email.header.make_header(email.header.decode_header(subject)))
|
||||||
|
@ -301,11 +333,13 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
|
||||||
# Procesar email adjunto
|
# Procesar email adjunto
|
||||||
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos)
|
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos)
|
||||||
mensajes.extend(mensajes_adjuntos)
|
mensajes.extend(mensajes_adjuntos)
|
||||||
elif parte.get_content_disposition() == 'attachment':
|
elif parte.get_content_disposition() == "attachment":
|
||||||
nombre = parte.get_filename()
|
nombre = parte.get_filename()
|
||||||
if nombre and nombre.lower().endswith('.eml'):
|
if nombre and nombre.lower().endswith(".eml"):
|
||||||
# Si es un archivo .eml adjunto
|
# Si es un archivo .eml adjunto
|
||||||
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos)
|
mensajes_adjuntos = _procesar_email_adjunto(
|
||||||
|
parte, dir_adjuntos
|
||||||
|
)
|
||||||
mensajes.extend(mensajes_adjuntos)
|
mensajes.extend(mensajes_adjuntos)
|
||||||
else:
|
else:
|
||||||
# Otros tipos de adjuntos
|
# Otros tipos de adjuntos
|
||||||
|
@ -327,32 +361,54 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
|
||||||
|
|
||||||
# Solo agregar el mensaje si tiene contenido útil
|
# Solo agregar el mensaje si tiene contenido útil
|
||||||
if contenido or subject or adjuntos:
|
if contenido or subject or adjuntos:
|
||||||
mensajes.append(MensajeEmail(
|
mensaje_nuevo = MensajeEmail(
|
||||||
remitente=remitente,
|
remitente=remitente,
|
||||||
fecha=fecha,
|
fecha=fecha,
|
||||||
contenido=contenido,
|
contenido=contenido,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
adjuntos=adjuntos
|
adjuntos=adjuntos,
|
||||||
))
|
)
|
||||||
|
print(f" ✉️ Mensaje extraído:")
|
||||||
|
print(f" - Subject: {subject}")
|
||||||
|
print(f" - Remitente: {remitente}")
|
||||||
|
print(f" - Fecha: {fecha}")
|
||||||
|
print(f" - Adjuntos: {len(adjuntos)} archivos")
|
||||||
|
print(f" - Contenido: {len(contenido)} caracteres")
|
||||||
|
print(f" - Hash generado: {mensaje_nuevo.hash}")
|
||||||
|
mensajes.append(mensaje_nuevo)
|
||||||
|
else:
|
||||||
|
print(f" ⚠️ Mensaje vacío o sin contenido útil - no se agregará")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error procesando mensaje: {str(e)}")
|
print(f"Error procesando mensaje: {str(e)}")
|
||||||
|
|
||||||
return mensajes
|
return mensajes
|
||||||
|
|
||||||
|
|
||||||
def _parsear_fecha(fecha_str):
|
def _parsear_fecha(fecha_str):
|
||||||
try:
|
try:
|
||||||
fecha = parsedate_to_datetime(fecha_str)
|
fecha = parsedate_to_datetime(fecha_str)
|
||||||
return fecha.replace(tzinfo=None) # Remove timezone info
|
return fecha.replace(tzinfo=None) # Remove timezone info
|
||||||
except:
|
except:
|
||||||
try:
|
try:
|
||||||
fecha_match = re.search(r'venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})', fecha_str)
|
fecha_match = re.search(
|
||||||
|
r"venerd=EC (\d{1,2}) (\w+) (\d{4}) (\d{1,2}):(\d{2})", fecha_str
|
||||||
|
)
|
||||||
if fecha_match:
|
if fecha_match:
|
||||||
dia, mes, año, hora, minuto = fecha_match.groups()
|
dia, mes, año, hora, minuto = fecha_match.groups()
|
||||||
meses_it = {
|
meses_it = {
|
||||||
'gennaio': 1, 'febbraio': 2, 'marzo': 3, 'aprile': 4,
|
"gennaio": 1,
|
||||||
'maggio': 5, 'giugno': 6, 'luglio': 7, 'agosto': 8,
|
"febbraio": 2,
|
||||||
'settembre': 9, 'ottobre': 10, 'novembre': 11, 'dicembre': 12
|
"marzo": 3,
|
||||||
|
"aprile": 4,
|
||||||
|
"maggio": 5,
|
||||||
|
"giugno": 6,
|
||||||
|
"luglio": 7,
|
||||||
|
"agosto": 8,
|
||||||
|
"settembre": 9,
|
||||||
|
"ottobre": 10,
|
||||||
|
"novembre": 11,
|
||||||
|
"dicembre": 12,
|
||||||
}
|
}
|
||||||
mes_num = meses_it.get(mes.lower(), 1)
|
mes_num = meses_it.get(mes.lower(), 1)
|
||||||
return datetime(int(año), mes_num, int(dia), int(hora), int(minuto))
|
return datetime(int(año), mes_num, int(dia), int(hora), int(minuto))
|
||||||
|
|
|
@ -4,36 +4,138 @@ import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from models.mensaje_email import MensajeEmail
|
from models.mensaje_email import MensajeEmail
|
||||||
|
|
||||||
|
|
||||||
def cargar_cronologia_existente(archivo):
|
def cargar_cronologia_existente(archivo):
|
||||||
|
"""
|
||||||
|
Carga mensajes existentes desde un archivo de cronología en formato markdown.
|
||||||
|
Formato esperado:
|
||||||
|
+ hash
|
||||||
|
|
||||||
|
### Subject
|
||||||
|
|
||||||
|
- fecha
|
||||||
|
|
||||||
|
Contenido...
|
||||||
|
|
||||||
|
### Adjuntos (opcional)
|
||||||
|
- [[archivo1]]
|
||||||
|
---
|
||||||
|
"""
|
||||||
mensajes = []
|
mensajes = []
|
||||||
if not os.path.exists(archivo):
|
if not os.path.exists(archivo):
|
||||||
|
print(f"Archivo de cronología no existe: {archivo}")
|
||||||
return mensajes
|
return mensajes
|
||||||
|
|
||||||
with open(archivo, 'r', encoding='utf-8') as f:
|
print(f"Cargando cronología existente desde: {archivo}")
|
||||||
|
|
||||||
|
with open(archivo, "r", encoding="utf-8") as f:
|
||||||
contenido = f.read()
|
contenido = f.read()
|
||||||
|
|
||||||
bloques = contenido.split('---\n\n')
|
# Saltar el índice inicial si existe (hasta el primer ---\n\n)
|
||||||
for bloque in bloques:
|
if contenido.startswith("# Índice de Mensajes"):
|
||||||
|
partes = contenido.split("---\n\n", 1)
|
||||||
|
if len(partes) > 1:
|
||||||
|
contenido = partes[1]
|
||||||
|
else:
|
||||||
|
print("Archivo solo contiene índice, no hay mensajes")
|
||||||
|
return mensajes
|
||||||
|
|
||||||
|
# Dividir por separadores de mensaje
|
||||||
|
bloques = contenido.split("---\n\n")
|
||||||
|
|
||||||
|
print(f"Encontrados {len(bloques)} bloques para procesar")
|
||||||
|
|
||||||
|
for i, bloque in enumerate(bloques):
|
||||||
if not bloque.strip():
|
if not bloque.strip():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
match = re.match(r'## (\d{14})\|(.*?)\n\n(.*)', bloque.strip(), re.DOTALL)
|
try:
|
||||||
if match:
|
# Buscar el patrón: + hash, ### subject, - fecha, contenido
|
||||||
fecha_str, remitente, contenido = match.groups()
|
lineas = bloque.strip().split("\n")
|
||||||
fecha = datetime.strptime(fecha_str, '%Y%m%d%H%M%S')
|
|
||||||
|
|
||||||
|
# Encontrar hash
|
||||||
|
hash_msg = None
|
||||||
|
subject = None
|
||||||
|
fecha = None
|
||||||
|
contenido_inicio = 0
|
||||||
|
|
||||||
|
for j, linea in enumerate(lineas):
|
||||||
|
if linea.startswith("+ ") and not hash_msg:
|
||||||
|
hash_msg = linea[2:].strip()
|
||||||
|
elif linea.startswith("### ") and not subject:
|
||||||
|
subject = linea[4:].strip()
|
||||||
|
elif linea.startswith("- ") and not fecha:
|
||||||
|
try:
|
||||||
|
fecha_str = linea[2:].strip()
|
||||||
|
fecha = datetime.strptime(fecha_str, "%d-%m-%Y")
|
||||||
|
contenido_inicio = j + 1
|
||||||
|
break
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not hash_msg or not fecha:
|
||||||
|
print(f"Bloque {i+1}: No se pudo extraer hash o fecha, saltando")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Extraer contenido y adjuntos
|
||||||
|
contenido_lineas = lineas[contenido_inicio:]
|
||||||
adjuntos = []
|
adjuntos = []
|
||||||
if '### Adjuntos' in contenido:
|
|
||||||
contenido_principal, lista_adjuntos = contenido.split('### Adjuntos')
|
|
||||||
adjuntos = [adj.strip()[2:-2] for adj in lista_adjuntos.strip().split('\n')]
|
|
||||||
contenido = contenido_principal.strip()
|
|
||||||
|
|
||||||
mensajes.append(MensajeEmail(
|
# Buscar sección de adjuntos
|
||||||
|
contenido_texto = []
|
||||||
|
en_adjuntos = False
|
||||||
|
|
||||||
|
for linea in contenido_lineas:
|
||||||
|
if linea.strip() == "### Adjuntos":
|
||||||
|
en_adjuntos = True
|
||||||
|
continue
|
||||||
|
|
||||||
|
if en_adjuntos:
|
||||||
|
if linea.startswith("- [[") and linea.endswith("]]"):
|
||||||
|
adjunto = linea[4:-2] # Remover - [[ y ]]
|
||||||
|
adjuntos.append(adjunto)
|
||||||
|
else:
|
||||||
|
contenido_texto.append(linea)
|
||||||
|
|
||||||
|
# Unir contenido sin líneas vacías al final
|
||||||
|
contenido_final = "\n".join(contenido_texto).strip()
|
||||||
|
|
||||||
|
# Crear el mensaje sin especificar remitente ya que no está en el formato actual
|
||||||
|
# El remitente se extraerá del contenido si es posible
|
||||||
|
remitente = "Remitente Desconocido" # valor por defecto
|
||||||
|
|
||||||
|
# Intentar extraer remitente del contenido
|
||||||
|
for linea in contenido_final.split("\n")[
|
||||||
|
:5
|
||||||
|
]: # revisar solo las primeras líneas
|
||||||
|
if "From:" in linea or "De:" in linea or "Da:" in linea:
|
||||||
|
remitente = linea.strip()
|
||||||
|
break
|
||||||
|
|
||||||
|
mensaje = MensajeEmail(
|
||||||
remitente=remitente,
|
remitente=remitente,
|
||||||
fecha=fecha,
|
fecha=fecha,
|
||||||
contenido=contenido,
|
contenido=contenido_final,
|
||||||
adjuntos=adjuntos
|
subject=subject,
|
||||||
))
|
adjuntos=adjuntos,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verificar que el hash coincida para validar
|
||||||
|
if mensaje.hash == hash_msg:
|
||||||
|
mensajes.append(mensaje)
|
||||||
|
print(f"✓ Mensaje cargado: {subject} - {fecha.strftime('%d-%m-%Y')}")
|
||||||
|
else:
|
||||||
|
print(f"⚠ Hash no coincide para mensaje {subject}, regenerando...")
|
||||||
|
# Si el hash no coincide, usar el hash original del archivo
|
||||||
|
mensaje.hash = hash_msg
|
||||||
|
mensajes.append(mensaje)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error procesando bloque {i+1}: {str(e)}")
|
||||||
|
print(f"Contenido del bloque: {bloque[:200]}...")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"Carga completada: {len(mensajes)} mensajes cargados desde cronología existente"
|
||||||
|
)
|
||||||
return mensajes
|
return mensajes
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"path": "C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email",
|
"path": "D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\Entregado por VM\\01 - 26-07-2025 Max - Emails",
|
||||||
"history": [
|
"history": [
|
||||||
|
"D:\\Trabajo\\VM\\45 - HENKEL - VM Auto Changeover\\Entregado por VM\\01 - 26-07-2025 Max - Emails",
|
||||||
|
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\03-VM\\45 - HENKEL - VM Auto Changeover",
|
||||||
"C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email",
|
"C:\\Trabajo\\SIDEL\\14 - E5.007172 - Modifica O&U - SAE340\\Reporte\\Email",
|
||||||
"C:\\Trabajo\\SIDEL\\12 - SAE052 - Syrup Update & GSD Update\\Reporte\\Emails",
|
"C:\\Trabajo\\SIDEL\\12 - SAE052 - Syrup Update & GSD Update\\Reporte\\Emails",
|
||||||
"C:\\Trabajo\\SIDEL\\10 - E5.007095 - Modifica O&U - SAE463\\Reporte\\Email",
|
"C:\\Trabajo\\SIDEL\\10 - E5.007095 - Modifica O&U - SAE463\\Reporte\\Email",
|
||||||
|
@ -8,8 +10,6 @@
|
||||||
"C:\\Trabajo\\SIDEL\\EMAILs\\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS",
|
"C:\\Trabajo\\SIDEL\\EMAILs\\I_ E5.007727 _ Evo On - SFSRFH300172 + SFSRFH300109 - ANDIA LACTEOS",
|
||||||
"C:\\Estudio",
|
"C:\\Estudio",
|
||||||
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody",
|
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody",
|
||||||
"C:\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Reporte\\Emails",
|
"C:\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Reporte\\Emails"
|
||||||
"C:\\Users\\migue\\OneDrive\\Miguel\\Obsidean\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Emails",
|
|
||||||
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\Emails\\Trial"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -6,9 +6,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from utils.email_parser import procesar_eml
|
from utils.email_parser import procesar_eml
|
||||||
from utils.markdown_handler import cargar_cronologia_existente
|
|
||||||
from utils.beautify import BeautifyProcessor
|
from utils.beautify import BeautifyProcessor
|
||||||
import json
|
|
||||||
script_root = os.path.dirname(
|
script_root = os.path.dirname(
|
||||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||||
)
|
)
|
||||||
|
@ -34,9 +33,17 @@ def generar_indice(mensajes):
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Cargar configuraciones del entorno
|
# Cargar configuraciones del entorno
|
||||||
# configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
|
|
||||||
configs = load_configuration()
|
configs = load_configuration()
|
||||||
|
|
||||||
|
# Verificar si la configuración se cargó según backend_setup.md
|
||||||
|
if not configs:
|
||||||
|
print("Error: No se pudo cargar la configuración. Verificar script_config.json")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
print("✅ Configuración cargada exitosamente")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Obtener working directory
|
# Obtener working directory
|
||||||
working_directory = configs.get("working_directory", ".")
|
working_directory = configs.get("working_directory", ".")
|
||||||
|
|
||||||
|
@ -46,27 +53,29 @@ def main():
|
||||||
attachments_dir = group_config.get("attachments_dir", "adjuntos")
|
attachments_dir = group_config.get("attachments_dir", "adjuntos")
|
||||||
|
|
||||||
work_config = configs.get("level3", {})
|
work_config = configs.get("level3", {})
|
||||||
outpu_directory = work_config.get("output_directory", ".")
|
output_directory = work_config.get("output_directory", ".")
|
||||||
|
|
||||||
# Construir rutas absolutas
|
# Construir rutas absolutas
|
||||||
input_dir = (
|
input_dir = (
|
||||||
working_directory # El directorio de trabajo es el directorio de entrada
|
working_directory # El directorio de trabajo es el directorio de entrada
|
||||||
)
|
)
|
||||||
output_file = os.path.join(outpu_directory, cronologia_file)
|
output_file = os.path.join(output_directory, cronologia_file)
|
||||||
attachments_path = os.path.join(working_directory, attachments_dir)
|
attachments_path = os.path.join(working_directory, attachments_dir)
|
||||||
|
|
||||||
# Debug prints
|
# Debug prints
|
||||||
print(f"Working directory: {working_directory}")
|
print(f"Working directory: {working_directory}")
|
||||||
print(f"Input directory: {input_dir}")
|
print(f"Input directory: {input_dir}")
|
||||||
print(f"Output directory: {outpu_directory}")
|
print(f"Output directory: {output_directory}")
|
||||||
print(f"Cronologia file: {output_file}")
|
print(f"Cronologia file: {output_file}")
|
||||||
print(f"Attachments directory: {attachments_path}")
|
print(f"Attachments directory: {attachments_path}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Obtener el directorio donde está el script actual
|
# Obtener el directorio donde está el script actual
|
||||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json")
|
beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json")
|
||||||
beautifier = BeautifyProcessor(beautify_rules)
|
beautifier = BeautifyProcessor(beautify_rules)
|
||||||
print(f"Beautify rules file: {beautify_rules}")
|
print(f"Beautify rules file: {beautify_rules}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Ensure directories exist
|
# Ensure directories exist
|
||||||
os.makedirs(attachments_path, exist_ok=True)
|
os.makedirs(attachments_path, exist_ok=True)
|
||||||
|
@ -75,39 +84,82 @@ def main():
|
||||||
input_path = Path(input_dir)
|
input_path = Path(input_dir)
|
||||||
if not input_path.exists():
|
if not input_path.exists():
|
||||||
print(f"Error: Input directory {input_path} does not exist")
|
print(f"Error: Input directory {input_path} does not exist")
|
||||||
|
sys.stdout.flush()
|
||||||
return
|
return
|
||||||
|
|
||||||
eml_files = list(input_path.glob("*.eml"))
|
eml_files = list(input_path.glob("*.eml"))
|
||||||
print(f"Found {len(eml_files)} .eml files")
|
print(f"Found {len(eml_files)} .eml files")
|
||||||
|
|
||||||
|
if not eml_files:
|
||||||
|
print("⚠️ No se encontraron archivos .eml en el directorio")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Crear cronología nueva (no cargar existente)
|
||||||
mensajes = []
|
mensajes = []
|
||||||
print(f"Loaded {len(mensajes)} existing messages")
|
mensajes_hash = set()
|
||||||
mensajes_hash = {msg.hash for msg in mensajes}
|
print("Creando cronología nueva (archivo se sobrescribirá)")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
total_procesados = 0
|
total_procesados = 0
|
||||||
total_nuevos = 0
|
total_nuevos = 0
|
||||||
mensajes_duplicados = 0
|
mensajes_duplicados = 0
|
||||||
|
|
||||||
for archivo in eml_files:
|
for archivo in eml_files:
|
||||||
print(f"\nProcessing {archivo}")
|
print(f"\n{'='*60}")
|
||||||
|
print(f"Processing file: {archivo}")
|
||||||
|
sys.stdout.flush()
|
||||||
nuevos_mensajes = procesar_eml(archivo, attachments_path)
|
nuevos_mensajes = procesar_eml(archivo, attachments_path)
|
||||||
|
print(f"Extracted {len(nuevos_mensajes)} messages from {archivo.name}")
|
||||||
|
sys.stdout.flush()
|
||||||
total_procesados += len(nuevos_mensajes)
|
total_procesados += len(nuevos_mensajes)
|
||||||
|
|
||||||
# Verificar duplicados y aplicar beautify solo a los mensajes nuevos
|
# Verificar duplicados y aplicar beautify solo a los mensajes nuevos
|
||||||
for msg in nuevos_mensajes:
|
for i, msg in enumerate(nuevos_mensajes):
|
||||||
|
print(f"\n--- Message {i+1}/{len(nuevos_mensajes)} from {archivo.name} ---")
|
||||||
|
print(f"Remitente: {msg.remitente}")
|
||||||
|
print(f"Fecha: {msg.fecha}")
|
||||||
|
print(f"Subject: {msg.subject}")
|
||||||
|
print(f"Hash: {msg.hash}")
|
||||||
|
print(f"Adjuntos: {msg.adjuntos}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
if msg.hash not in mensajes_hash:
|
if msg.hash not in mensajes_hash:
|
||||||
|
print("✓ NUEVO mensaje - Agregando a la cronología")
|
||||||
|
sys.stdout.flush()
|
||||||
# Aplicar beautify solo si el mensaje es nuevo
|
# Aplicar beautify solo si el mensaje es nuevo
|
||||||
msg.contenido = beautifier.process_text(msg.contenido)
|
msg.contenido = beautifier.process_text(msg.contenido)
|
||||||
mensajes.append(msg)
|
mensajes.append(msg)
|
||||||
mensajes_hash.add(msg.hash)
|
mensajes_hash.add(msg.hash)
|
||||||
total_nuevos += 1
|
total_nuevos += 1
|
||||||
else:
|
else:
|
||||||
|
print("⚠ DUPLICADO - Ya existe un mensaje con este hash")
|
||||||
mensajes_duplicados += 1
|
mensajes_duplicados += 1
|
||||||
|
# Buscar el mensaje duplicado para mostrar información detallada
|
||||||
|
for existing_msg in mensajes:
|
||||||
|
if existing_msg.hash == msg.hash:
|
||||||
|
print(" 📋 Comparación de mensajes duplicados:")
|
||||||
|
print(" Mensaje existente:")
|
||||||
|
print(f" - Remitente: {existing_msg.remitente}")
|
||||||
|
print(f" - Fecha: {existing_msg.fecha}")
|
||||||
|
print(f" - Subject: {existing_msg.subject}")
|
||||||
|
print(" Mensaje nuevo (rechazado):")
|
||||||
|
print(f" - Remitente: {msg.remitente}")
|
||||||
|
print(f" - Fecha: {msg.fecha}")
|
||||||
|
print(f" - Subject: {msg.subject}")
|
||||||
|
|
||||||
|
# Mostrar debug detallado del hash para ambos mensajes
|
||||||
|
print(" 🔍 Debug detallado del hash duplicado:")
|
||||||
|
print(f" Hash: {msg.hash}")
|
||||||
|
msg.debug_hash_info()
|
||||||
|
sys.stdout.flush()
|
||||||
|
break
|
||||||
|
|
||||||
print("\nEstadísticas de procesamiento:")
|
print("\nEstadísticas de procesamiento:")
|
||||||
print("- Total mensajes encontrados:", total_procesados)
|
print("- Total mensajes encontrados:", total_procesados)
|
||||||
print("- Mensajes únicos añadidos:", total_nuevos)
|
print("- Mensajes únicos añadidos:", total_nuevos)
|
||||||
print("- Mensajes duplicados ignorados:", mensajes_duplicados)
|
print("- Mensajes duplicados ignorados:", mensajes_duplicados)
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
# Ordenar mensajes de más reciente a más antiguo
|
# Ordenar mensajes de más reciente a más antiguo
|
||||||
mensajes.sort(key=lambda x: x.fecha, reverse=True)
|
mensajes.sort(key=lambda x: x.fecha, reverse=True)
|
||||||
|
@ -117,6 +169,9 @@ def main():
|
||||||
|
|
||||||
# Escribir el archivo con el índice y los mensajes
|
# Escribir el archivo con el índice y los mensajes
|
||||||
print(f"\nWriting {len(mensajes)} messages to {output_file}")
|
print(f"\nWriting {len(mensajes)} messages to {output_file}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
try:
|
||||||
with open(output_file, "w", encoding="utf-8") as f:
|
with open(output_file, "w", encoding="utf-8") as f:
|
||||||
# Primero escribir el índice
|
# Primero escribir el índice
|
||||||
f.write(indice)
|
f.write(indice)
|
||||||
|
@ -124,6 +179,15 @@ def main():
|
||||||
for msg in mensajes:
|
for msg in mensajes:
|
||||||
f.write(msg.to_markdown())
|
f.write(msg.to_markdown())
|
||||||
|
|
||||||
|
print(f"✅ Cronología guardada exitosamente en: {output_file}")
|
||||||
|
print(f"📊 Total de mensajes en la cronología: {len(mensajes)}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"❌ Error al guardar la cronología: {e}")
|
||||||
|
sys.stdout.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
},
|
},
|
||||||
"level2": {},
|
"level2": {},
|
||||||
"level3": {},
|
"level3": {},
|
||||||
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport"
|
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport"
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",
|
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
|
||||||
"history": [
|
"history": [
|
||||||
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
|
||||||
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",
|
||||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
|
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
"xref_source_subdir": "source"
|
"xref_source_subdir": "source"
|
||||||
},
|
},
|
||||||
"level3": {},
|
"level3": {},
|
||||||
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport"
|
"working_directory": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport"
|
||||||
}
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",
|
"path": "C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
|
||||||
"history": [
|
"history": [
|
||||||
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giorgio in Bosco\\Reporte\\TiaExport",
|
||||||
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",
|
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\TiaExport",
|
||||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||||
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||||
|
|
|
@ -1,5 +1,83 @@
|
||||||
{
|
{
|
||||||
"history": [
|
"history": [
|
||||||
|
{
|
||||||
|
"id": "e8aa982b",
|
||||||
|
"group_id": "2",
|
||||||
|
"script_name": "main.py",
|
||||||
|
"executed_date": "2025-07-29T16:05:04.334751Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 24560,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "32f31000",
|
||||||
|
"group_id": "2",
|
||||||
|
"script_name": "main.py",
|
||||||
|
"executed_date": "2025-07-28T11:07:27.240046Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 32692,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fc0a57b7",
|
||||||
|
"group_id": "2",
|
||||||
|
"script_name": "main.py",
|
||||||
|
"executed_date": "2025-07-22T15:44:52.285146Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 19992,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "d6c698e9",
|
||||||
|
"group_id": "2",
|
||||||
|
"script_name": "main.py",
|
||||||
|
"executed_date": "2025-07-21T15:04:24.680517Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 30356,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "98a1228a",
|
||||||
|
"group_id": "2",
|
||||||
|
"script_name": "main.py",
|
||||||
|
"executed_date": "2025-07-18T15:21:08.672411Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 43768,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "e2ab479e",
|
||||||
|
"group_id": "2",
|
||||||
|
"script_name": "main.py",
|
||||||
|
"executed_date": "2025-07-17T11:55:05.330647Z",
|
||||||
|
"arguments": [],
|
||||||
|
"working_directory": "D:/Proyectos/Scripts/RS485/MaselliSimulatorApp",
|
||||||
|
"python_env": "tia_scripting",
|
||||||
|
"executable_type": "pythonw.exe",
|
||||||
|
"status": "running",
|
||||||
|
"pid": 30736,
|
||||||
|
"execution_time": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "e7060cbc",
|
"id": "e7060cbc",
|
||||||
"group_id": "2",
|
"group_id": "2",
|
||||||
|
|
29318
data/log.txt
29318
data/log.txt
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue