Mejorado del Log de Procesador de Emails a Cronología

This commit is contained in:
Miguel 2025-07-30 10:56:06 +02:00
parent f0f45df1b8
commit 4fa955f71b
16 changed files with 1909 additions and 28699 deletions

View File

@ -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.

View File

@ -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).

View File

@ -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)

View File

@ -4,15 +4,16 @@ 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()
def _formatear_subject_para_link(self, subject): def _formatear_subject_para_link(self, subject):
""" """
Formatea el subject para usarlo como ancla en links de Obsidian Formatea el subject para usarlo como ancla en links de Obsidian
@ -21,59 +22,61 @@ 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):
if not contenido: if not contenido:
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()
def to_markdown(self): def to_markdown(self):
# Hash con caracteres no título # Hash con caracteres no título
hash_line = f"+ {self.hash}\n\n" hash_line = f"+ {self.hash}\n\n"
# Subject como título # Subject como título
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
md = f"{hash_line}{subject_line}{fecha_line}" md = f"{hash_line}{subject_line}{fecha_line}"
md += self.contenido + "\n\n" md += self.contenido + "\n\n"
# Adjuntos si existen # Adjuntos si existen
if self.adjuntos: if self.adjuntos:
md += "### Adjuntos\n" md += "### Adjuntos\n"
@ -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

View File

@ -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"
} }

View File

@ -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,37 +12,42 @@ 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)
def _load_rules(self, rules_file): def _load_rules(self, rules_file):
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":
pattern = self._convert_block_pattern_to_regex(pattern) pattern = self._convert_block_pattern_to_regex(pattern)
pattern_type = PatternType.REGEX pattern_type = PatternType.REGEX
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}")
continue continue
@ -51,12 +57,12 @@ class BeautifyProcessor:
except Exception as e: except Exception as e:
print(f"Error procesando regla: {e}") print(f"Error procesando regla: {e}")
continue continue
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
print(f"Error decodificando JSON: {e}") print(f"Error decodificando JSON: {e}")
except Exception as e: except Exception as e:
print(f"Error cargando reglas: {e}") print(f"Error cargando reglas: {e}")
return rules_by_priority return rules_by_priority
def _convert_block_pattern_to_regex(self, pattern): def _convert_block_pattern_to_regex(self, pattern):
@ -67,42 +73,51 @@ class BeautifyProcessor:
# Reemplazar temporalmente los ..... con un marcador único # Reemplazar temporalmente los ..... con un marcador único
marker = "__BLOCK_MARKER__" marker = "__BLOCK_MARKER__"
pattern = pattern.replace(".....", marker) pattern = pattern.replace(".....", marker)
# Escapar caracteres especiales # Escapar caracteres especiales
pattern = re.escape(pattern) pattern = re.escape(pattern)
# 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
matches = list(pattern.finditer(result)) matches = list(pattern.finditer(result))
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:]
return result return result
def _line_matches(self, line, pattern, pattern_type): def _line_matches(self, line, pattern, pattern_type):
@ -134,54 +149,63 @@ 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):
if not text: if not text:
return text return text
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
return result return result
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:
print(f"Error procesando archivo {input_file}: {e}") print(f"Error procesando archivo {input_file}: {e}")
@ -189,28 +213,28 @@ class BeautifyProcessor:
lines = text.splitlines() lines = text.splitlines()
result_lines = [] result_lines = []
skip_next_empty = False skip_next_empty = False
for i, line in enumerate(lines): for i, line in enumerate(lines):
should_remove = self._line_matches(line, pattern, pattern_type) should_remove = self._line_matches(line, pattern, pattern_type)
if should_remove: if should_remove:
if i < len(lines) - 1 and not lines[i + 1].strip(): if i < len(lines) - 1 and not lines[i + 1].strip():
skip_next_empty = True skip_next_empty = True
continue continue
if skip_next_empty and not line.strip(): if skip_next_empty and not line.strip():
skip_next_empty = False skip_next_empty = False
continue continue
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()
result_lines = [] result_lines = []
for line in lines: for line in lines:
if self._line_matches(line, pattern, pattern_type): if self._line_matches(line, pattern, pattern_type):
if action == "add_before": if action == "add_before":
@ -221,5 +245,5 @@ class BeautifyProcessor:
result_lines.append(replacement) result_lines.append(replacement)
else: else:
result_lines.append(line) result_lines.append(line)
return '\n'.join(result_lines) return "\n".join(result_lines)

View File

@ -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,99 +23,117 @@ 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
""" """
if html is None: if html is None:
return (None, "") return (None, "")
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
# Matriz para almacenar la tabla procesada # Matriz para almacenar la tabla procesada
table_matrix = [] table_matrix = []
max_cols = 0 max_cols = 0
# Primera pasada: crear matriz y procesar rowspans/colspans # Primera pasada: crear matriz y procesar rowspans/colspans
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
# Expandir matriz si es necesario # Expandir matriz si es necesario
while len(table_matrix) <= row_idx: while len(table_matrix) <= row_idx:
table_matrix.append([]) table_matrix.append([])
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
for r in range(rowspan): for r in range(rowspan):
current_row = row_idx + r current_row = row_idx + r
@ -122,90 +141,97 @@ 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
max_cols = max(max_cols, col_idx) max_cols = max(max_cols, col_idx)
row_idx += 1 row_idx += 1
# 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()
# Procesar líneas # Procesar líneas
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.
@ -231,45 +257,51 @@ def _procesar_email_adjunto(parte, dir_adjuntos):
mensajes.extend(procesar_eml_interno(msg, dir_adjuntos)) mensajes.extend(procesar_eml_interno(msg, dir_adjuntos))
elif isinstance(payload, email.message.Message): elif isinstance(payload, email.message.Message):
mensajes.extend(procesar_eml_interno(payload, dir_adjuntos)) mensajes.extend(procesar_eml_interno(payload, dir_adjuntos))
return mensajes return mensajes
except Exception as e: except Exception as e:
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
""" """
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)))
contenido = "" contenido = ""
adjuntos = [] adjuntos = []
tiene_html = False tiene_html = False
# First pass: check for HTML content # First pass: check for HTML content
if mensaje.is_multipart(): if mensaje.is_multipart():
for parte in mensaje.walk(): for parte in mensaje.walk():
@ -278,12 +310,12 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
break break
else: else:
tiene_html = mensaje.get_content_type() == "text/html" tiene_html = mensaje.get_content_type() == "text/html"
# Second pass: process content and attachments # Second pass: process content and attachments
if mensaje.is_multipart(): if mensaje.is_multipart():
for parte in mensaje.walk(): for parte in mensaje.walk():
content_type = parte.get_content_type() content_type = parte.get_content_type()
try: try:
if content_type == "text/html": if content_type == "text/html":
html_content = _get_payload_safely(parte) html_content = _get_payload_safely(parte)
@ -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
@ -324,38 +358,60 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
subject = part_subject subject = part_subject
else: else:
contenido = _get_payload_safely(mensaje) or "" contenido = _get_payload_safely(mensaje) or ""
# 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))
except: except:
pass pass
return datetime.now() return datetime.now()

View File

@ -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')
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( # 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 = []
# 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

View File

@ -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"
] ]
} }

View File

@ -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,12 +169,24 @@ 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}")
with open(output_file, "w", encoding="utf-8") as f: sys.stdout.flush()
# Primero escribir el índice
f.write(indice) try:
# Luego escribir todos los mensajes with open(output_file, "w", encoding="utf-8") as f:
for msg in mensajes: # Primero escribir el índice
f.write(msg.to_markdown()) f.write(indice)
# Luego escribir todos los mensajes
for msg in mensajes:
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__":

View File

@ -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"
} }

View File

@ -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",

View File

@ -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"
} }

View File

@ -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",

View File

@ -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

File diff suppressed because it is too large Load Diff