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 email.utils import parseaddr, parsedate_to_datetime
|
||||
|
||||
|
||||
class MensajeEmail:
|
||||
def __init__(self, remitente, fecha, contenido, subject=None, adjuntos=None):
|
||||
self.remitente = self._estandarizar_remitente(remitente)
|
||||
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.adjuntos = adjuntos if adjuntos else []
|
||||
self.hash = self._generar_hash()
|
||||
|
@ -21,8 +22,8 @@ class MensajeEmail:
|
|||
if not subject:
|
||||
return "Sin-Asunto"
|
||||
# Eliminar caracteres especiales y reemplazar espacios con guiones
|
||||
formatted = re.sub(r'[^\w\s-]', '', subject)
|
||||
formatted = re.sub(r'\s+', '-', formatted.strip())
|
||||
formatted = re.sub(r"[^\w\s-]", "", subject)
|
||||
formatted = re.sub(r"\s+", "-", formatted.strip())
|
||||
return formatted
|
||||
|
||||
def _limpiar_contenido(self, contenido):
|
||||
|
@ -30,31 +31,33 @@ class MensajeEmail:
|
|||
return ""
|
||||
|
||||
# Eliminar líneas de metadatos
|
||||
lines = contenido.split('\n')
|
||||
lines = contenido.split("\n")
|
||||
cleaned_lines = []
|
||||
|
||||
for line in 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
|
||||
# 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)
|
||||
|
||||
# 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
|
||||
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
|
||||
text = text.replace('\r\n', '\n')
|
||||
text = text.replace("\r\n", "\n")
|
||||
|
||||
# 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
|
||||
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
|
||||
return text.strip()
|
||||
|
@ -67,7 +70,7 @@ class MensajeEmail:
|
|||
subject_line = f"### {self.subject if self.subject else 'Sin Asunto'}\n\n"
|
||||
|
||||
# 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"
|
||||
|
||||
# Contenido del mensaje
|
||||
|
@ -86,28 +89,28 @@ class MensajeEmail:
|
|||
"""
|
||||
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)
|
||||
return f"- {fecha_formato} - {self.remitente} - [[cronologia#{self.subject}|{subject_link}]]"
|
||||
|
||||
def _estandarizar_remitente(self, remitente):
|
||||
if 'Da:' in remitente:
|
||||
remitente = remitente.split('Da:')[1].split('Inviato:')[0]
|
||||
elif 'From:' in remitente:
|
||||
remitente = remitente.split('From:')[1].split('Sent:')[0]
|
||||
if "Da:" in remitente:
|
||||
remitente = remitente.split("Da:")[1].split("Inviato:")[0]
|
||||
elif "From:" in remitente:
|
||||
remitente = remitente.split("From:")[1].split("Sent:")[0]
|
||||
|
||||
nombre, email = parseaddr(remitente)
|
||||
if not nombre and email:
|
||||
nombre = email.split('@')[0]
|
||||
nombre = email.split("@")[0]
|
||||
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:
|
||||
nombre = nombre_match.group(1)
|
||||
else:
|
||||
return "Remitente Desconocido"
|
||||
|
||||
nombre = re.sub(r'[<>:"/\\|?*]', '', nombre.strip())
|
||||
nombre = nombre.encode('ascii', 'ignore').decode('ascii')
|
||||
nombre = re.sub(r'[<>:"/\\|?*]', "", nombre.strip())
|
||||
nombre = nombre.encode("ascii", "ignore").decode("ascii")
|
||||
return nombre
|
||||
|
||||
def _estandarizar_fecha(self, fecha):
|
||||
|
@ -125,21 +128,47 @@ class MensajeEmail:
|
|||
"""
|
||||
# Limpiar y normalizar el contenido para el hash
|
||||
# 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
|
||||
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
|
||||
elementos_hash = [
|
||||
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,
|
||||
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
|
||||
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
|
||||
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"
|
||||
},
|
||||
"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 enum import Enum
|
||||
|
||||
|
||||
class PatternType(Enum):
|
||||
REGEX = "regex"
|
||||
STRING = "string"
|
||||
|
@ -11,6 +12,7 @@ class PatternType(Enum):
|
|||
RIGHT = "right"
|
||||
SUBSTRING = "substring"
|
||||
|
||||
|
||||
class BeautifyProcessor:
|
||||
def __init__(self, rules_file):
|
||||
self.rules_by_priority = self._load_rules(rules_file)
|
||||
|
@ -19,19 +21,21 @@ class BeautifyProcessor:
|
|||
rules_by_priority = defaultdict(list)
|
||||
|
||||
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)
|
||||
|
||||
if not isinstance(data, dict) or 'rules' not in data:
|
||||
raise ValueError("El archivo JSON debe contener un objeto con una clave 'rules'")
|
||||
if not isinstance(data, dict) or "rules" not in data:
|
||||
raise ValueError(
|
||||
"El archivo JSON debe contener un objeto con una clave 'rules'"
|
||||
)
|
||||
|
||||
for rule in data['rules']:
|
||||
for rule in data["rules"]:
|
||||
try:
|
||||
pattern = rule['pattern']
|
||||
replacement = rule['replacement']
|
||||
action = rule['action']
|
||||
pattern_type = PatternType(rule.get('type', 'string'))
|
||||
priority = int(rule.get('priority', 999))
|
||||
pattern = rule["pattern"]
|
||||
replacement = rule["replacement"]
|
||||
action = rule["action"]
|
||||
pattern_type = PatternType(rule.get("type", "string"))
|
||||
priority = int(rule.get("priority", 999))
|
||||
|
||||
# Para remove_block, convertir el patrón con ..... a una regex
|
||||
if action == "remove_block":
|
||||
|
@ -40,7 +44,9 @@ class BeautifyProcessor:
|
|||
elif pattern_type == PatternType.REGEX:
|
||||
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:
|
||||
print(f"Error en regla: falta campo requerido {e}")
|
||||
|
@ -74,7 +80,7 @@ class BeautifyProcessor:
|
|||
# Restaurar el marcador con el patrón .*?
|
||||
pattern = pattern.replace(marker, ".*?")
|
||||
|
||||
return re.compile(f'(?s){pattern}')
|
||||
return re.compile(f"(?s){pattern}")
|
||||
|
||||
def _process_remove_block(self, text, pattern):
|
||||
result = text
|
||||
|
@ -83,22 +89,31 @@ class BeautifyProcessor:
|
|||
for match in reversed(matches):
|
||||
start, end = match.span()
|
||||
|
||||
line_start = result.rfind('\n', 0, start) + 1
|
||||
line_start = result.rfind("\n", 0, start) + 1
|
||||
if line_start == 0:
|
||||
line_start = 0
|
||||
|
||||
line_end = result.find('\n', end)
|
||||
line_end = result.find("\n", end)
|
||||
if line_end == -1:
|
||||
line_end = len(result)
|
||||
else:
|
||||
line_end += 1
|
||||
|
||||
while line_start > 0 and result[line_start-1:line_start] == '\n' and \
|
||||
(line_start == 1 or result[line_start-2:line_start-1] == '\n'):
|
||||
while (
|
||||
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
|
||||
|
||||
while 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'):
|
||||
while (
|
||||
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
|
||||
|
||||
result = result[:line_start] + result[line_end:]
|
||||
|
@ -134,16 +149,20 @@ class BeautifyProcessor:
|
|||
result_lines.append(line.replace(pattern, replacement, 1))
|
||||
else:
|
||||
result_lines.append(line)
|
||||
return '\n'.join(result_lines)
|
||||
return "\n".join(result_lines)
|
||||
elif pattern_type == PatternType.RIGHT:
|
||||
lines = text.splitlines()
|
||||
result_lines = []
|
||||
for line in lines:
|
||||
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:
|
||||
result_lines.append(line)
|
||||
return '\n'.join(result_lines)
|
||||
return "\n".join(result_lines)
|
||||
return text
|
||||
|
||||
def process_text(self, text):
|
||||
|
@ -153,18 +172,23 @@ class BeautifyProcessor:
|
|||
result = text
|
||||
for priority in sorted(self.rules_by_priority.keys()):
|
||||
rules = self.rules_by_priority[priority]
|
||||
print(f"Aplicando reglas de prioridad {priority}")
|
||||
|
||||
for pattern, replacement, action, pattern_type in rules:
|
||||
try:
|
||||
if action == "remove_block":
|
||||
result = self._process_remove_block(result, pattern)
|
||||
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":
|
||||
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"]:
|
||||
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:
|
||||
print(f"Error aplicando regla {pattern}: {e}")
|
||||
continue
|
||||
|
@ -173,13 +197,13 @@ class BeautifyProcessor:
|
|||
|
||||
def process_file(self, input_file, output_file=None):
|
||||
try:
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
with open(input_file, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
processed_content = self.process_text(content)
|
||||
|
||||
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)
|
||||
|
||||
except Exception as e:
|
||||
|
@ -205,7 +229,7 @@ class BeautifyProcessor:
|
|||
result_lines.append(line)
|
||||
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):
|
||||
lines = text.splitlines()
|
||||
|
@ -222,4 +246,4 @@ class BeautifyProcessor:
|
|||
else:
|
||||
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 os
|
||||
|
||||
|
||||
def _get_payload_safely(parte):
|
||||
"""
|
||||
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)
|
||||
if payload is None:
|
||||
return None
|
||||
charset = parte.get_content_charset() or 'utf-8'
|
||||
return payload.decode(charset, errors='ignore')
|
||||
charset = parte.get_content_charset() or "utf-8"
|
||||
return payload.decode(charset, errors="ignore")
|
||||
except Exception as e:
|
||||
print(f"Error getting payload: {str(e)}")
|
||||
return None
|
||||
|
||||
|
||||
def _extract_subject_from_text(text):
|
||||
"""
|
||||
Extrae el asunto de un texto dados diferentes formatos de cabecera
|
||||
"""
|
||||
subject_headers = {
|
||||
'Oggetto: ': 9, # Italian
|
||||
'Subject: ': 9, # English
|
||||
'Asunto: ': 8, # Spanish
|
||||
'Sujet: ': 7, # French
|
||||
'Betreff: ': 9 # German
|
||||
"Oggetto: ": 9, # Italian
|
||||
"Subject: ": 9, # English
|
||||
"Asunto: ": 8, # Spanish
|
||||
"Sujet: ": 7, # French
|
||||
"Betreff: ": 9, # German
|
||||
}
|
||||
|
||||
for line in text.split('\n'):
|
||||
for line in text.split("\n"):
|
||||
line = line.strip()
|
||||
for header, offset in subject_headers.items():
|
||||
if line.startswith(header):
|
||||
return line[offset:].strip()
|
||||
return None
|
||||
|
||||
|
||||
def _should_skip_line(line):
|
||||
"""
|
||||
Determina si una línea debe ser omitida por ser una cabecera de email
|
||||
"""
|
||||
headers_to_skip = [
|
||||
'Da: ', 'Inviato: ', 'A: ', # Italian
|
||||
'From: ', 'Sent: ', 'To: ', # English
|
||||
'De: ', 'Enviado: ', 'Para: ', # Spanish
|
||||
'Von: ', 'Gesendet: ', 'An: ', # German
|
||||
'De : ', 'Envoyé : ', 'À : ' # French
|
||||
"Da: ",
|
||||
"Inviato: ",
|
||||
"A: ", # Italian
|
||||
"From: ",
|
||||
"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)
|
||||
|
||||
|
||||
def _html_a_markdown(html):
|
||||
"""
|
||||
Convierte contenido HTML a texto markdown, extrayendo el asunto si está presente
|
||||
|
@ -69,16 +83,16 @@ def _html_a_markdown(html):
|
|||
|
||||
try:
|
||||
# Limpieza básica
|
||||
html = html.replace('\xa0', ' ') # NBSP a espacio normal
|
||||
html = html.replace('\r\n', '\n') # CRLF a LF
|
||||
html = html.replace('\r', '\n') # CR a LF
|
||||
html = html.replace("\xa0", " ") # NBSP a espacio normal
|
||||
html = html.replace("\r\n", "\n") # CRLF a LF
|
||||
html = html.replace("\r", "\n") # CR a LF
|
||||
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
|
||||
# Procesar tablas
|
||||
for table in soup.find_all('table'):
|
||||
for table in soup.find_all("table"):
|
||||
try:
|
||||
rows = table.find_all('tr')
|
||||
rows = table.find_all("tr")
|
||||
if not rows:
|
||||
continue
|
||||
|
||||
|
@ -90,7 +104,7 @@ def _html_a_markdown(html):
|
|||
row_idx = 0
|
||||
while row_idx < len(rows):
|
||||
row = rows[row_idx]
|
||||
cells = row.find_all(['th', 'td'])
|
||||
cells = row.find_all(["th", "td"])
|
||||
if not cells:
|
||||
row_idx += 1
|
||||
continue
|
||||
|
@ -102,17 +116,22 @@ def _html_a_markdown(html):
|
|||
col_idx = 0
|
||||
for cell in cells:
|
||||
# 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
|
||||
|
||||
# Obtener rowspan y colspan
|
||||
rowspan = int(cell.get('rowspan', 1))
|
||||
colspan = int(cell.get('colspan', 1))
|
||||
rowspan = int(cell.get("rowspan", 1))
|
||||
colspan = int(cell.get("colspan", 1))
|
||||
|
||||
# Procesar el texto de la celda reemplazando saltos de línea por <br>
|
||||
cell_text = cell.get_text().strip()
|
||||
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 = cell_text.replace("\n", "<br>")
|
||||
cell_text = re.sub(
|
||||
r"\s*<br>\s*<br>\s*", "<br>", cell_text
|
||||
) # Eliminar <br> múltiples
|
||||
cell_text = cell_text.strip()
|
||||
|
||||
# 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:
|
||||
table_matrix.append([])
|
||||
# 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)
|
||||
|
||||
for c in range(colspan):
|
||||
if r == 0 and c == 0:
|
||||
table_matrix[current_row][col_idx + c] = cell_text
|
||||
else:
|
||||
table_matrix[current_row][col_idx + c] = ''
|
||||
table_matrix[current_row][col_idx + c] = ""
|
||||
|
||||
col_idx += colspan
|
||||
|
||||
|
@ -139,51 +160,55 @@ def _html_a_markdown(html):
|
|||
# Asegurar que todas las filas tengan el mismo número de columnas
|
||||
for row in table_matrix:
|
||||
while len(row) < max_cols:
|
||||
row.append('')
|
||||
row.append("")
|
||||
|
||||
# Calcular anchos máximos por columna
|
||||
col_widths = [0] * max_cols
|
||||
for row in table_matrix:
|
||||
for col_idx, cell in enumerate(row):
|
||||
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
|
||||
markdown_table = []
|
||||
|
||||
# Cabecera
|
||||
if table_matrix:
|
||||
header = '|'
|
||||
header = "|"
|
||||
for col_idx, width in enumerate(col_widths):
|
||||
cell = str(table_matrix[0][col_idx] or '')
|
||||
header += f' {cell.ljust(width)} |'
|
||||
cell = str(table_matrix[0][col_idx] or "")
|
||||
header += f" {cell.ljust(width)} |"
|
||||
markdown_table.append(header)
|
||||
|
||||
# Separador
|
||||
separator = '|'
|
||||
separator = "|"
|
||||
for width in col_widths:
|
||||
separator += '-' * (width + 2) + '|'
|
||||
separator += "-" * (width + 2) + "|"
|
||||
markdown_table.append(separator)
|
||||
|
||||
# Contenido
|
||||
for row_idx in range(1, len(table_matrix)):
|
||||
row_text = '|'
|
||||
row_text = "|"
|
||||
for col_idx, width in enumerate(col_widths):
|
||||
cell = str(table_matrix[row_idx][col_idx] or '')
|
||||
row_text += f' {cell.ljust(width)} |'
|
||||
cell = str(table_matrix[row_idx][col_idx] or "")
|
||||
row_text += f" {cell.ljust(width)} |"
|
||||
markdown_table.append(row_text)
|
||||
|
||||
# Reemplazar la tabla HTML con la versión Markdown
|
||||
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:
|
||||
print(f"Error procesando tabla: {str(e)}")
|
||||
continue
|
||||
|
||||
# Procesar saltos de línea
|
||||
for br in soup.find_all('br'):
|
||||
br.replace_with('\n')
|
||||
for br in soup.find_all("br"):
|
||||
br.replace_with("\n")
|
||||
|
||||
# Obtener texto limpio
|
||||
text = soup.get_text()
|
||||
|
@ -192,20 +217,21 @@ def _html_a_markdown(html):
|
|||
cleaned_lines = []
|
||||
subject = None
|
||||
|
||||
for line in text.split('\n'):
|
||||
for line in text.split("\n"):
|
||||
if not subject:
|
||||
subject = _extract_subject_from_text(line)
|
||||
|
||||
if not _should_skip_line(line):
|
||||
cleaned_lines.append(line)
|
||||
|
||||
final_text = '\n'.join(cleaned_lines).strip()
|
||||
final_text = "\n".join(cleaned_lines).strip()
|
||||
return (subject, final_text)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error en html_a_markdown: {str(e)}")
|
||||
return (None, html if html else "")
|
||||
|
||||
|
||||
def _procesar_email_adjunto(parte, dir_adjuntos):
|
||||
"""
|
||||
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)}")
|
||||
return []
|
||||
|
||||
|
||||
def procesar_eml(ruta_archivo, dir_adjuntos):
|
||||
"""
|
||||
Punto de entrada principal para procesar archivos .eml
|
||||
"""
|
||||
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)
|
||||
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:
|
||||
print(f"Error al abrir el archivo {ruta_archivo}: {str(e)}")
|
||||
print(f"❌ Error al abrir el archivo {ruta_archivo}: {str(e)}")
|
||||
return []
|
||||
|
||||
|
||||
def procesar_eml_interno(mensaje, dir_adjuntos):
|
||||
"""
|
||||
Procesa un mensaje de email, ya sea desde archivo o adjunto
|
||||
|
@ -256,12 +288,12 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
|
|||
mensajes = []
|
||||
|
||||
try:
|
||||
remitente = mensaje.get('from', '')
|
||||
fecha_str = mensaje.get('date', '')
|
||||
remitente = mensaje.get("from", "")
|
||||
fecha_str = mensaje.get("date", "")
|
||||
fecha = _parsear_fecha(fecha_str)
|
||||
|
||||
# Get subject from email headers first
|
||||
subject = mensaje.get('subject', '')
|
||||
subject = mensaje.get("subject", "")
|
||||
if subject:
|
||||
# Try to decode if it's encoded
|
||||
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
|
||||
mensajes_adjuntos = _procesar_email_adjunto(parte, dir_adjuntos)
|
||||
mensajes.extend(mensajes_adjuntos)
|
||||
elif parte.get_content_disposition() == 'attachment':
|
||||
elif parte.get_content_disposition() == "attachment":
|
||||
nombre = parte.get_filename()
|
||||
if nombre and nombre.lower().endswith('.eml'):
|
||||
if nombre and nombre.lower().endswith(".eml"):
|
||||
# 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)
|
||||
else:
|
||||
# Otros tipos de adjuntos
|
||||
|
@ -327,32 +361,54 @@ def procesar_eml_interno(mensaje, dir_adjuntos):
|
|||
|
||||
# Solo agregar el mensaje si tiene contenido útil
|
||||
if contenido or subject or adjuntos:
|
||||
mensajes.append(MensajeEmail(
|
||||
mensaje_nuevo = MensajeEmail(
|
||||
remitente=remitente,
|
||||
fecha=fecha,
|
||||
contenido=contenido,
|
||||
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:
|
||||
print(f"Error procesando mensaje: {str(e)}")
|
||||
|
||||
return mensajes
|
||||
|
||||
|
||||
def _parsear_fecha(fecha_str):
|
||||
try:
|
||||
fecha = parsedate_to_datetime(fecha_str)
|
||||
return fecha.replace(tzinfo=None) # Remove timezone info
|
||||
except:
|
||||
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:
|
||||
dia, mes, año, hora, minuto = fecha_match.groups()
|
||||
meses_it = {
|
||||
'gennaio': 1, 'febbraio': 2, 'marzo': 3, 'aprile': 4,
|
||||
'maggio': 5, 'giugno': 6, 'luglio': 7, 'agosto': 8,
|
||||
'settembre': 9, 'ottobre': 10, 'novembre': 11, 'dicembre': 12
|
||||
"gennaio": 1,
|
||||
"febbraio": 2,
|
||||
"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)
|
||||
return datetime(int(año), mes_num, int(dia), int(hora), int(minuto))
|
||||
|
|
|
@ -4,36 +4,138 @@ import re
|
|||
from datetime import datetime
|
||||
from models.mensaje_email import MensajeEmail
|
||||
|
||||
|
||||
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 = []
|
||||
if not os.path.exists(archivo):
|
||||
print(f"Archivo de cronología no existe: {archivo}")
|
||||
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()
|
||||
|
||||
bloques = contenido.split('---\n\n')
|
||||
for bloque in bloques:
|
||||
# Saltar el índice inicial si existe (hasta el primer ---\n\n)
|
||||
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():
|
||||
continue
|
||||
|
||||
match = re.match(r'## (\d{14})\|(.*?)\n\n(.*)', bloque.strip(), re.DOTALL)
|
||||
if match:
|
||||
fecha_str, remitente, contenido = match.groups()
|
||||
fecha = datetime.strptime(fecha_str, '%Y%m%d%H%M%S')
|
||||
try:
|
||||
# Buscar el patrón: + hash, ### subject, - fecha, contenido
|
||||
lineas = bloque.strip().split("\n")
|
||||
|
||||
# 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 = []
|
||||
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,
|
||||
fecha=fecha,
|
||||
contenido=contenido,
|
||||
adjuntos=adjuntos
|
||||
))
|
||||
contenido=contenido_final,
|
||||
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
|
||||
|
||||
|
|
|
@ -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": [
|
||||
"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\\12 - SAE052 - Syrup Update & GSD Update\\Reporte\\Emails",
|
||||
"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:\\Estudio",
|
||||
"C:\\Trabajo\\VM\\40 - 93040 - HENKEL - NEXT2 Problem\\Reporte\\EmailTody",
|
||||
"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"
|
||||
"C:\\Trabajo\\VM\\30 - 9.3941- Kosme - Portogallo (Modifica + Linea)\\Reporte\\Emails"
|
||||
]
|
||||
}
|
|
@ -6,9 +6,8 @@ import os
|
|||
import sys
|
||||
from pathlib import Path
|
||||
from utils.email_parser import procesar_eml
|
||||
from utils.markdown_handler import cargar_cronologia_existente
|
||||
from utils.beautify import BeautifyProcessor
|
||||
import json
|
||||
|
||||
script_root = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
|
@ -34,9 +33,17 @@ def generar_indice(mensajes):
|
|||
|
||||
def main():
|
||||
# Cargar configuraciones del entorno
|
||||
# configs = json.loads(os.environ.get("SCRIPT_CONFIGS", "{}"))
|
||||
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
|
||||
working_directory = configs.get("working_directory", ".")
|
||||
|
||||
|
@ -46,27 +53,29 @@ def main():
|
|||
attachments_dir = group_config.get("attachments_dir", "adjuntos")
|
||||
|
||||
work_config = configs.get("level3", {})
|
||||
outpu_directory = work_config.get("output_directory", ".")
|
||||
output_directory = work_config.get("output_directory", ".")
|
||||
|
||||
# Construir rutas absolutas
|
||||
input_dir = (
|
||||
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)
|
||||
|
||||
# Debug prints
|
||||
print(f"Working directory: {working_directory}")
|
||||
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"Attachments directory: {attachments_path}")
|
||||
sys.stdout.flush()
|
||||
|
||||
# Obtener el directorio donde está el script actual
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
beautify_rules = os.path.join(script_dir, "config", "beautify_rules.json")
|
||||
beautifier = BeautifyProcessor(beautify_rules)
|
||||
print(f"Beautify rules file: {beautify_rules}")
|
||||
sys.stdout.flush()
|
||||
|
||||
# Ensure directories exist
|
||||
os.makedirs(attachments_path, exist_ok=True)
|
||||
|
@ -75,39 +84,82 @@ def main():
|
|||
input_path = Path(input_dir)
|
||||
if not input_path.exists():
|
||||
print(f"Error: Input directory {input_path} does not exist")
|
||||
sys.stdout.flush()
|
||||
return
|
||||
|
||||
eml_files = list(input_path.glob("*.eml"))
|
||||
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 = []
|
||||
print(f"Loaded {len(mensajes)} existing messages")
|
||||
mensajes_hash = {msg.hash for msg in mensajes}
|
||||
mensajes_hash = set()
|
||||
print("Creando cronología nueva (archivo se sobrescribirá)")
|
||||
sys.stdout.flush()
|
||||
|
||||
total_procesados = 0
|
||||
total_nuevos = 0
|
||||
mensajes_duplicados = 0
|
||||
|
||||
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)
|
||||
print(f"Extracted {len(nuevos_mensajes)} messages from {archivo.name}")
|
||||
sys.stdout.flush()
|
||||
total_procesados += len(nuevos_mensajes)
|
||||
|
||||
# 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:
|
||||
print("✓ NUEVO mensaje - Agregando a la cronología")
|
||||
sys.stdout.flush()
|
||||
# Aplicar beautify solo si el mensaje es nuevo
|
||||
msg.contenido = beautifier.process_text(msg.contenido)
|
||||
mensajes.append(msg)
|
||||
mensajes_hash.add(msg.hash)
|
||||
total_nuevos += 1
|
||||
else:
|
||||
print("⚠ DUPLICADO - Ya existe un mensaje con este hash")
|
||||
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("- Total mensajes encontrados:", total_procesados)
|
||||
print("- Mensajes únicos añadidos:", total_nuevos)
|
||||
print("- Mensajes duplicados ignorados:", mensajes_duplicados)
|
||||
sys.stdout.flush()
|
||||
|
||||
# Ordenar mensajes de más reciente a más antiguo
|
||||
mensajes.sort(key=lambda x: x.fecha, reverse=True)
|
||||
|
@ -117,6 +169,9 @@ def main():
|
|||
|
||||
# Escribir el archivo con el índice y los mensajes
|
||||
print(f"\nWriting {len(mensajes)} messages to {output_file}")
|
||||
sys.stdout.flush()
|
||||
|
||||
try:
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
# Primero escribir el índice
|
||||
f.write(indice)
|
||||
|
@ -124,6 +179,15 @@ def main():
|
|||
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__":
|
||||
main()
|
||||
|
|
|
@ -5,5 +5,5 @@
|
|||
},
|
||||
"level2": {},
|
||||
"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": [
|
||||
"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",
|
||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia",
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
"xref_source_subdir": "source"
|
||||
},
|
||||
"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": [
|
||||
"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",
|
||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||
|
|
|
@ -1,5 +1,83 @@
|
|||
{
|
||||
"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",
|
||||
"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