Compare commits
No commits in common. "13ceda63bad1da126bb7b5af256bb284d931c433" and "b74db36cf9d395d5e96753c6294066f19050101d" have entirely different histories.
13ceda63ba
...
b74db36cf9
|
@ -6,8 +6,6 @@ __pycache__/
|
|||
# C extensions
|
||||
*.so
|
||||
|
||||
backend/script_groups/TwinCat/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
temp/
|
||||
|
|
242
app.py
242
app.py
|
@ -3,7 +3,6 @@ from flask_sock import Sock
|
|||
from lib.config_manager import ConfigurationManager
|
||||
from lib.launcher_manager import LauncherManager
|
||||
from lib.csharp_launcher_manager import CSharpLauncherManager
|
||||
from lib.python_launcher_manager import PythonLauncherManager
|
||||
import os
|
||||
import json # Added import
|
||||
from datetime import datetime
|
||||
|
@ -32,9 +31,6 @@ launcher_manager = LauncherManager(config_manager.data_path)
|
|||
# Inicializar C# launcher manager
|
||||
csharp_launcher_manager = CSharpLauncherManager(config_manager.data_path)
|
||||
|
||||
# Inicializar Python launcher manager
|
||||
python_launcher_manager = PythonLauncherManager(config_manager.data_path)
|
||||
|
||||
# Lista global para mantener las conexiones WebSocket activas
|
||||
websocket_connections = set()
|
||||
|
||||
|
@ -995,199 +991,6 @@ def get_all_csharp_executables(project_id):
|
|||
|
||||
# === FIN C# LAUNCHER APIs ===
|
||||
|
||||
|
||||
# === PYTHON LAUNCHER APIs ===
|
||||
|
||||
@app.route("/api/python-projects", methods=["GET", "POST"])
|
||||
def handle_python_projects():
|
||||
"""Gestionar proyectos Python (GET: obtener, POST: crear)"""
|
||||
if request.method == "GET":
|
||||
try:
|
||||
projects = python_launcher_manager.get_python_projects()
|
||||
return jsonify(projects)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
else: # POST
|
||||
try:
|
||||
data = request.json
|
||||
result = python_launcher_manager.add_python_project(data)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-projects/<project_id>", methods=["GET", "PUT", "DELETE"])
|
||||
def handle_python_project(project_id):
|
||||
"""Gestionar proyecto Python específico (GET: obtener, PUT: actualizar, DELETE: eliminar)"""
|
||||
if request.method == "GET":
|
||||
try:
|
||||
project = python_launcher_manager.get_python_project(project_id)
|
||||
if not project:
|
||||
return jsonify({"error": "Project not found"}), 404
|
||||
return jsonify(project)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
elif request.method == "PUT":
|
||||
try:
|
||||
data = request.json
|
||||
result = python_launcher_manager.update_python_project(project_id, data)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
else: # DELETE
|
||||
try:
|
||||
result = python_launcher_manager.delete_python_project(project_id)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-scripts/<project_id>")
|
||||
def get_python_scripts(project_id):
|
||||
"""Obtener scripts de un proyecto Python"""
|
||||
try:
|
||||
scripts = python_launcher_manager.get_project_scripts(project_id)
|
||||
return jsonify(scripts)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-scripts-all/<project_id>")
|
||||
def get_all_python_scripts(project_id):
|
||||
"""Obtener TODOS los scripts de un proyecto Python (incluyendo ocultos) para gestión"""
|
||||
try:
|
||||
scripts = python_launcher_manager.get_all_project_scripts(project_id)
|
||||
return jsonify(scripts)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-script-metadata/<project_id>/<script_name>", methods=["GET", "POST"])
|
||||
def handle_python_script_metadata(project_id, script_name):
|
||||
"""Gestionar metadatos de un script Python específico"""
|
||||
if request.method == "GET":
|
||||
try:
|
||||
metadata = python_launcher_manager.get_script_metadata(project_id, script_name)
|
||||
return jsonify(metadata)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
else: # POST
|
||||
try:
|
||||
data = request.json
|
||||
result = python_launcher_manager.update_script_metadata(project_id, script_name, data)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/execute-python-script", methods=["POST"])
|
||||
def execute_python_script():
|
||||
"""Ejecutar script Python con argumentos opcionales"""
|
||||
try:
|
||||
data = request.json
|
||||
project_id = data["project_id"]
|
||||
script_name = data["script_name"]
|
||||
script_args = data.get("args", [])
|
||||
working_dir = data.get("working_dir", None)
|
||||
run_in_background = data.get("run_in_background", False) # Para servidores MCP, Flask, etc.
|
||||
|
||||
result = python_launcher_manager.execute_python_script(
|
||||
project_id, script_name, script_args, broadcast_message, working_dir, run_in_background
|
||||
)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
error_msg = f"Error ejecutando script Python: {str(e)}"
|
||||
broadcast_message(error_msg)
|
||||
return jsonify({"error": error_msg}), 500
|
||||
|
||||
@app.route("/api/python-favorites", methods=["GET", "POST"])
|
||||
def handle_python_favorites():
|
||||
"""Gestionar favoritos del launcher Python"""
|
||||
if request.method == "GET":
|
||||
try:
|
||||
favorites = python_launcher_manager.get_favorites()
|
||||
return jsonify({"favorites": favorites})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
else: # POST
|
||||
try:
|
||||
data = request.json
|
||||
project_id = data["project_id"]
|
||||
script_name = data["script_name"]
|
||||
result = python_launcher_manager.toggle_favorite(project_id, script_name)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-history", methods=["GET", "DELETE"])
|
||||
def handle_python_history():
|
||||
"""Gestionar historial del launcher Python"""
|
||||
if request.method == "GET":
|
||||
try:
|
||||
history = python_launcher_manager.get_history()
|
||||
return jsonify({"history": history})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
else: # DELETE
|
||||
try:
|
||||
result = python_launcher_manager.clear_history()
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-categories")
|
||||
def get_python_categories():
|
||||
"""Obtener categorías disponibles del launcher Python"""
|
||||
try:
|
||||
categories = python_launcher_manager.get_categories()
|
||||
return jsonify(categories)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-running-processes")
|
||||
def get_python_running_processes():
|
||||
"""Obtener procesos Python en ejecución"""
|
||||
try:
|
||||
processes = python_launcher_manager.get_running_processes()
|
||||
return jsonify({"processes": processes})
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-process-terminate/<int:pid>", methods=["POST"])
|
||||
def terminate_python_process(pid):
|
||||
"""Cerrar un proceso Python"""
|
||||
try:
|
||||
result = python_launcher_manager.terminate_process(pid)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-process-focus/<int:pid>", methods=["POST"])
|
||||
def focus_python_process(pid):
|
||||
"""Activar foco de un proceso Python"""
|
||||
try:
|
||||
result = python_launcher_manager.focus_process(pid)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
@app.route("/api/python-markdown/<project_id>")
|
||||
def get_python_markdown_files(project_id):
|
||||
"""Obtener archivos Markdown de un proyecto Python"""
|
||||
try:
|
||||
markdown_files = python_launcher_manager.get_markdown_files(project_id)
|
||||
return jsonify({"files": markdown_files})
|
||||
except Exception as e:
|
||||
print(f"Error getting markdown files for Python project {project_id}: {e}")
|
||||
# Devolver lista vacía en lugar de error para no interferir con scripts
|
||||
return jsonify({"files": []})
|
||||
|
||||
@app.route("/api/python-markdown-content/<project_id>/<path:relative_path>")
|
||||
def get_python_markdown_content(project_id, relative_path):
|
||||
"""Obtener contenido de un archivo Markdown de un proyecto Python"""
|
||||
try:
|
||||
result = python_launcher_manager.read_markdown_file(project_id, relative_path)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
# === FIN PYTHON LAUNCHER APIs ===
|
||||
|
||||
# --- Helper function to find VS Code ---
|
||||
def find_vscode_executable():
|
||||
"""Intenta encontrar el ejecutable de VS Code en ubicaciones comunes y en el PATH."""
|
||||
|
@ -1255,23 +1058,10 @@ def open_group_in_editor(editor, group_system, group_id):
|
|||
"status": "error",
|
||||
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
|
||||
}), 404
|
||||
elif group_system == 'python':
|
||||
project = python_launcher_manager.get_python_project(group_id)
|
||||
if not project:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Proyecto Python '{group_id}' no encontrado"
|
||||
}), 404
|
||||
script_group_path = project["directory"]
|
||||
if not os.path.isdir(script_group_path):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
|
||||
}), 404
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
|
||||
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
|
||||
}), 400
|
||||
|
||||
# Definir rutas de ejecutables
|
||||
|
@ -1386,23 +1176,10 @@ def open_group_folder(group_system, group_id):
|
|||
"status": "error",
|
||||
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
|
||||
}), 404
|
||||
elif group_system == 'python':
|
||||
project = python_launcher_manager.get_python_project(group_id)
|
||||
if not project:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Proyecto Python '{group_id}' no encontrado"
|
||||
}), 404
|
||||
script_group_path = project["directory"]
|
||||
if not os.path.isdir(script_group_path):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
|
||||
}), 404
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
|
||||
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
|
||||
}), 400
|
||||
|
||||
# Abrir en el explorador según el sistema operativo
|
||||
|
@ -1470,23 +1247,10 @@ def get_group_path(group_system, group_id):
|
|||
"status": "error",
|
||||
"message": f"Directorio del proyecto C# '{project['name']}' no encontrado"
|
||||
}), 404
|
||||
elif group_system == 'python':
|
||||
project = python_launcher_manager.get_python_project(group_id)
|
||||
if not project:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Proyecto Python '{group_id}' no encontrado"
|
||||
}), 404
|
||||
script_group_path = project["directory"]
|
||||
if not os.path.isdir(script_group_path):
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Directorio del proyecto Python '{project['name']}' no encontrado"
|
||||
}), 404
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher', 'csharp' o 'python'"
|
||||
"message": f"Sistema de grupo '{group_system}' no válido. Usar 'config', 'launcher' o 'csharp'"
|
||||
}), 400
|
||||
|
||||
return jsonify({
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/Analisis"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
|
@ -1,463 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para generar documentación de adaptación de IOs
|
||||
entre TwinCAT y TIA Portal - Proyecto SIDEL
|
||||
|
||||
Autor: Generado automáticamente
|
||||
Proyecto: E5.007560 - Modifica O&U - SAE235
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import pandas as pd
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import argparse
|
||||
from collections import defaultdict
|
||||
|
||||
# Configurar el path al directorio raíz del proyecto
|
||||
script_root = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
sys.path.append(script_root)
|
||||
|
||||
# Importar la función de configuración
|
||||
from backend.script_utils import load_configuration
|
||||
|
||||
|
||||
def load_tiaportal_adaptations(working_directory, file_path='IO Adapted.md'):
|
||||
"""Carga las adaptaciones de TIA Portal desde el archivo markdown"""
|
||||
full_file_path = os.path.join(working_directory, file_path)
|
||||
print(f"Cargando adaptaciones de TIA Portal desde: {full_file_path}")
|
||||
|
||||
adaptations = {}
|
||||
|
||||
if not os.path.exists(full_file_path):
|
||||
print(f"⚠️ Archivo {full_file_path} no encontrado")
|
||||
return adaptations
|
||||
|
||||
with open(full_file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Patrones mejorados para diferentes tipos de IOs
|
||||
patterns = [
|
||||
# Digitales: E0.0, A0.0
|
||||
r'\|\s*([EA]\d+\.\d+)\s*\|\s*([^|]+?)\s*\|',
|
||||
# Analógicos: PEW100, PAW100
|
||||
r'\|\s*(P[EA]W\d+)\s*\|\s*([^|]+?)\s*\|',
|
||||
# Profibus: EW 1640, AW 1640
|
||||
r'\|\s*([EA]W\s+\d+)\s*\|\s*([^|]+?)\s*\|'
|
||||
]
|
||||
|
||||
for pattern in patterns:
|
||||
matches = re.findall(pattern, content, re.MULTILINE)
|
||||
for io_addr, master_tag in matches:
|
||||
io_addr = io_addr.strip()
|
||||
master_tag = master_tag.strip()
|
||||
if io_addr and master_tag and not master_tag.startswith('-'):
|
||||
adaptations[io_addr] = master_tag
|
||||
print(f" 📍 {io_addr} → {master_tag}")
|
||||
|
||||
print(f"✅ Cargadas {len(adaptations)} adaptaciones de TIA Portal")
|
||||
return adaptations
|
||||
|
||||
def scan_twincat_definitions(working_directory, directory='TwinCat'):
|
||||
"""Escanea archivos TwinCAT para encontrar definiciones de variables AT %"""
|
||||
full_directory = os.path.join(working_directory, directory)
|
||||
print(f"\n🔍 Escaneando definiciones TwinCAT en: {full_directory}")
|
||||
|
||||
definitions = {}
|
||||
|
||||
if not os.path.exists(full_directory):
|
||||
print(f"⚠️ Directorio {full_directory} no encontrado")
|
||||
return definitions
|
||||
|
||||
# Patrones para definiciones AT %
|
||||
definition_patterns = [
|
||||
r'(\w+)\s+AT\s+%([IQ][XWB]\d+(?:\.\d+)?)\s*:\s*(\w+);', # Activas
|
||||
r'(\w+)\s+\(\*\s*AT\s+%([IQ][XWB]\d+(?:\.\d+)?)\s*\*\)\s*:\s*(\w+);', # Comentadas
|
||||
]
|
||||
|
||||
for file_path in Path(full_directory).glob('*.scl'):
|
||||
print(f" 📄 Procesando: {file_path.name}")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
content = f.read()
|
||||
|
||||
for pattern in definition_patterns:
|
||||
matches = re.findall(pattern, content, re.MULTILINE | re.IGNORECASE)
|
||||
for var_name, io_addr, data_type in matches:
|
||||
var_name = var_name.strip()
|
||||
io_addr = io_addr.strip()
|
||||
data_type = data_type.strip()
|
||||
|
||||
definitions[var_name] = {
|
||||
'address': io_addr,
|
||||
'type': data_type,
|
||||
'file': file_path.name,
|
||||
'definition_line': content[:content.find(var_name)].count('\n') + 1
|
||||
}
|
||||
print(f" 🔗 {var_name} AT %{io_addr} : {data_type}")
|
||||
|
||||
print(f"✅ Encontradas {len(definitions)} definiciones TwinCAT")
|
||||
return definitions
|
||||
|
||||
def scan_twincat_usage(working_directory, directory='TwinCat'):
|
||||
"""Escanea archivos TwinCAT para encontrar uso de variables"""
|
||||
full_directory = os.path.join(working_directory, directory)
|
||||
print(f"\n🔍 Escaneando uso de variables TwinCAT en: {full_directory}")
|
||||
|
||||
usage_data = defaultdict(list)
|
||||
|
||||
if not os.path.exists(full_directory):
|
||||
print(f"⚠️ Directorio {full_directory} no encontrado")
|
||||
return usage_data
|
||||
|
||||
for file_path in Path(full_directory).glob('*.scl'):
|
||||
print(f" 📄 Analizando uso en: {file_path.name}")
|
||||
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
for line_num, line in enumerate(lines, 1):
|
||||
# Buscar variables que empiecen con DI_, DO_, AI_, AO_
|
||||
var_matches = re.findall(r'\b([DA][IO]_\w+)\b', line)
|
||||
for var_name in var_matches:
|
||||
usage_data[var_name].append({
|
||||
'file': file_path.name,
|
||||
'line': line_num,
|
||||
'context': line.strip()[:100] + ('...' if len(line.strip()) > 100 else '')
|
||||
})
|
||||
|
||||
print(f"✅ Encontrado uso de {len(usage_data)} variables diferentes")
|
||||
return usage_data
|
||||
|
||||
def convert_tia_to_twincat(tia_addr):
|
||||
"""Convierte direcciones TIA Portal a formato TwinCAT"""
|
||||
conversions = []
|
||||
|
||||
# Digitales
|
||||
if re.match(r'^E\d+\.\d+$', tia_addr): # E0.0 → IX0.0
|
||||
twincat_addr = tia_addr.replace('E', 'IX')
|
||||
conversions.append(twincat_addr)
|
||||
elif re.match(r'^A\d+\.\d+$', tia_addr): # A0.0 → QX0.0
|
||||
twincat_addr = tia_addr.replace('A', 'QX')
|
||||
conversions.append(twincat_addr)
|
||||
|
||||
# Analógicos
|
||||
elif re.match(r'^PEW\d+$', tia_addr): # PEW100 → IW100
|
||||
twincat_addr = tia_addr.replace('PEW', 'IW')
|
||||
conversions.append(twincat_addr)
|
||||
elif re.match(r'^PAW\d+$', tia_addr): # PAW100 → QW100
|
||||
twincat_addr = tia_addr.replace('PAW', 'QW')
|
||||
conversions.append(twincat_addr)
|
||||
|
||||
# Profibus
|
||||
elif re.match(r'^EW\s+\d+$', tia_addr): # EW 1234 → IB1234
|
||||
addr_num = re.search(r'\d+', tia_addr).group()
|
||||
conversions.append(f'IB{addr_num}')
|
||||
elif re.match(r'^AW\s+\d+$', tia_addr): # AW 1234 → QB1234
|
||||
addr_num = re.search(r'\d+', tia_addr).group()
|
||||
conversions.append(f'QB{addr_num}')
|
||||
|
||||
return conversions
|
||||
|
||||
def find_variable_by_address(definitions, target_address):
|
||||
"""Busca variable por dirección exacta"""
|
||||
for var_name, info in definitions.items():
|
||||
if info['address'] == target_address:
|
||||
return var_name, info
|
||||
return None, None
|
||||
|
||||
def find_variable_by_name_similarity(definitions, usage_data, master_tag):
|
||||
"""Busca variables por similitud de nombre"""
|
||||
candidates = []
|
||||
|
||||
# Limpiar el master tag para comparación
|
||||
clean_master = re.sub(r'^[DA][IO]_', '', master_tag).lower()
|
||||
|
||||
# Buscar en definiciones
|
||||
for var_name, info in definitions.items():
|
||||
clean_var = re.sub(r'^[DA][IO]_', '', var_name).lower()
|
||||
if clean_master in clean_var or clean_var in clean_master:
|
||||
candidates.append((var_name, info, 'definition'))
|
||||
|
||||
# Buscar en uso
|
||||
for var_name in usage_data.keys():
|
||||
clean_var = re.sub(r'^[DA][IO]_', '', var_name).lower()
|
||||
if clean_master in clean_var or clean_var in clean_master:
|
||||
# Intentar encontrar la definición de esta variable
|
||||
var_info = definitions.get(var_name)
|
||||
if not var_info:
|
||||
var_info = {'address': 'Unknown', 'type': 'Unknown', 'file': 'Not found'}
|
||||
candidates.append((var_name, var_info, 'usage'))
|
||||
|
||||
return candidates
|
||||
|
||||
def analyze_adaptations(tia_adaptations, twincat_definitions, twincat_usage):
|
||||
"""Analiza las correlaciones entre TIA Portal y TwinCAT"""
|
||||
print(f"\n📊 Analizando correlaciones...")
|
||||
|
||||
results = []
|
||||
matches_found = 0
|
||||
|
||||
for tia_addr, master_tag in tia_adaptations.items():
|
||||
result = {
|
||||
'tia_address': tia_addr,
|
||||
'master_tag': master_tag,
|
||||
'twincat_variable': None,
|
||||
'twincat_address': None,
|
||||
'twincat_type': None,
|
||||
'match_type': None,
|
||||
'definition_file': None,
|
||||
'usage_files': [],
|
||||
'usage_count': 0,
|
||||
'confidence': 'Low'
|
||||
}
|
||||
|
||||
# 1. Buscar por conversión directa de dirección
|
||||
twincat_addresses = convert_tia_to_twincat(tia_addr)
|
||||
var_found = False
|
||||
|
||||
for twincat_addr in twincat_addresses:
|
||||
var_name, var_info = find_variable_by_address(twincat_definitions, twincat_addr)
|
||||
if var_name:
|
||||
result.update({
|
||||
'twincat_variable': var_name,
|
||||
'twincat_address': var_info['address'],
|
||||
'twincat_type': var_info['type'],
|
||||
'match_type': 'Address Match',
|
||||
'definition_file': var_info['file'],
|
||||
'confidence': 'High'
|
||||
})
|
||||
var_found = True
|
||||
matches_found += 1
|
||||
break
|
||||
|
||||
# 2. Si no se encontró por dirección, buscar por nombre
|
||||
if not var_found:
|
||||
candidates = find_variable_by_name_similarity(twincat_definitions, twincat_usage, master_tag)
|
||||
if candidates:
|
||||
# Tomar el mejor candidato
|
||||
best_candidate = candidates[0]
|
||||
var_name, var_info, source = best_candidate
|
||||
|
||||
result.update({
|
||||
'twincat_variable': var_name,
|
||||
'twincat_address': var_info.get('address', 'Unknown'),
|
||||
'twincat_type': var_info.get('type', 'Unknown'),
|
||||
'match_type': f'Name Similarity ({source})',
|
||||
'definition_file': var_info.get('file', 'Unknown'),
|
||||
'confidence': 'Medium'
|
||||
})
|
||||
matches_found += 1
|
||||
|
||||
# 3. Buscar información de uso
|
||||
if result['twincat_variable']:
|
||||
var_name = result['twincat_variable']
|
||||
if var_name in twincat_usage:
|
||||
usage_info = twincat_usage[var_name]
|
||||
result['usage_files'] = list(set([u['file'] for u in usage_info]))
|
||||
result['usage_count'] = len(usage_info)
|
||||
|
||||
results.append(result)
|
||||
|
||||
# Log del progreso
|
||||
status = "✅" if result['twincat_variable'] else "❌"
|
||||
print(f" {status} {tia_addr} → {master_tag}")
|
||||
if result['twincat_variable']:
|
||||
print(f" 🔗 {result['twincat_variable']} AT %{result['twincat_address']}")
|
||||
if result['usage_count'] > 0:
|
||||
print(f" 📝 Usado en {result['usage_count']} lugares: {', '.join(result['usage_files'])}")
|
||||
|
||||
print(f"\n🎯 Resumen: {matches_found}/{len(tia_adaptations)} variables correlacionadas ({matches_found/len(tia_adaptations)*100:.1f}%)")
|
||||
|
||||
return results
|
||||
|
||||
def create_results_directory(working_directory):
|
||||
"""Crea el directorio de resultados si no existe"""
|
||||
results_dir = Path(working_directory) / 'resultados'
|
||||
results_dir.mkdir(exist_ok=True)
|
||||
print(f"📁 Directorio de resultados: {results_dir.absolute()}")
|
||||
return results_dir
|
||||
|
||||
def generate_json_output(results, working_directory, output_file='io_adaptation_data.json'):
|
||||
"""Genera archivo JSON con datos estructurados para análisis posterior"""
|
||||
full_output_file = os.path.join(working_directory, 'resultados', output_file)
|
||||
print(f"\n📄 Generando archivo JSON: {full_output_file}")
|
||||
|
||||
json_data = {
|
||||
"metadata": {
|
||||
"generated_at": pd.Timestamp.now().isoformat(),
|
||||
"project": "E5.007560 - Modifica O&U - SAE235",
|
||||
"total_adaptations": len(results),
|
||||
"matched_variables": len([r for r in results if r['twincat_variable']]),
|
||||
"high_confidence": len([r for r in results if r['confidence'] == 'High']),
|
||||
"medium_confidence": len([r for r in results if r['confidence'] == 'Medium'])
|
||||
},
|
||||
"adaptations": []
|
||||
}
|
||||
|
||||
for result in results:
|
||||
adaptation = {
|
||||
"tia_portal": {
|
||||
"address": result['tia_address'],
|
||||
"tag": result['master_tag']
|
||||
},
|
||||
"twincat": {
|
||||
"variable": result['twincat_variable'],
|
||||
"address": result['twincat_address'],
|
||||
"data_type": result['twincat_type'],
|
||||
"definition_file": result['definition_file']
|
||||
},
|
||||
"correlation": {
|
||||
"match_type": result['match_type'],
|
||||
"confidence": result['confidence'],
|
||||
"found": result['twincat_variable'] is not None
|
||||
},
|
||||
"usage": {
|
||||
"usage_count": result['usage_count'],
|
||||
"usage_files": result['usage_files']
|
||||
}
|
||||
}
|
||||
json_data["adaptations"].append(adaptation)
|
||||
|
||||
with open(full_output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"✅ Archivo JSON generado: {full_output_file}")
|
||||
|
||||
def generate_detailed_report(results, working_directory, output_file='IO_Detailed_Analysis_Report.md'):
|
||||
"""Genera un reporte detallado con tabla markdown"""
|
||||
full_output_file = os.path.join(working_directory, 'resultados', output_file)
|
||||
print(f"\n📄 Generando reporte detallado: {full_output_file}")
|
||||
|
||||
with open(full_output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("# Reporte Detallado de Análisis de Adaptación IO\n\n")
|
||||
f.write(f"**Fecha de generación:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
|
||||
# Estadísticas
|
||||
total = len(results)
|
||||
matched = len([r for r in results if r['twincat_variable']])
|
||||
high_conf = len([r for r in results if r['confidence'] == 'High'])
|
||||
medium_conf = len([r for r in results if r['confidence'] == 'Medium'])
|
||||
|
||||
f.write("## 📊 Estadísticas Generales\n\n")
|
||||
f.write(f"- **Total adaptaciones procesadas:** {total}\n")
|
||||
f.write(f"- **Variables encontradas:** {matched} ({matched/total*100:.1f}%)\n")
|
||||
f.write(f"- **Coincidencias de alta confianza:** {high_conf}\n")
|
||||
f.write(f"- **Coincidencias de media confianza:** {medium_conf}\n\n")
|
||||
|
||||
# Tabla de variables correlacionadas exitosamente
|
||||
f.write("## ✅ Variables Correlacionadas Exitosamente\n\n")
|
||||
matched_results = [r for r in results if r['twincat_variable']]
|
||||
|
||||
if matched_results:
|
||||
# Encabezado de la tabla
|
||||
f.write("| TIA Address | TIA Tag | TwinCAT Variable | TwinCAT Address | Tipo | Método | Confianza | Archivo Def. | Uso | Archivos Uso |\n")
|
||||
f.write("|-------------|---------|------------------|-----------------|------|--------|-----------|--------------|-----|---------------|\n")
|
||||
|
||||
# Filas de datos
|
||||
for result in matched_results:
|
||||
usage_files_str = ', '.join(result['usage_files'][:3]) # Limitar a 3 archivos
|
||||
if len(result['usage_files']) > 3:
|
||||
usage_files_str += "..."
|
||||
|
||||
f.write(f"| {result['tia_address']} | "
|
||||
f"`{result['master_tag']}` | "
|
||||
f"`{result['twincat_variable']}` | "
|
||||
f"`%{result['twincat_address']}` | "
|
||||
f"`{result['twincat_type']}` | "
|
||||
f"{result['match_type']} | "
|
||||
f"{result['confidence']} | "
|
||||
f"{result['definition_file']} | "
|
||||
f"{result['usage_count']} | "
|
||||
f"{usage_files_str} |\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
# Tabla de variables no encontradas
|
||||
f.write("## ❌ Variables No Encontradas\n\n")
|
||||
unmatched_results = [r for r in results if not r['twincat_variable']]
|
||||
|
||||
if unmatched_results:
|
||||
f.write("| TIA Address | TIA Tag |\n")
|
||||
f.write("|-------------|----------|\n")
|
||||
|
||||
for result in unmatched_results:
|
||||
f.write(f"| {result['tia_address']} | `{result['master_tag']}` |\n")
|
||||
|
||||
f.write(f"\n**Total no encontradas:** {len(unmatched_results)}\n\n")
|
||||
|
||||
# Recomendaciones
|
||||
f.write("## 💡 Recomendaciones\n\n")
|
||||
f.write("1. **Variables de alta confianza** pueden migrarse directamente\n")
|
||||
f.write("2. **Variables de media confianza** requieren verificación manual\n")
|
||||
f.write("3. **Variables no encontradas** requieren mapeo manual o pueden ser obsoletas\n")
|
||||
f.write("4. Variables con uso extensivo son prioritarias para la migración\n\n")
|
||||
|
||||
# Resumen por confianza
|
||||
f.write("## 📈 Distribución por Confianza\n\n")
|
||||
f.write("| Nivel de Confianza | Cantidad | Porcentaje |\n")
|
||||
f.write("|--------------------|----------|------------|\n")
|
||||
f.write(f"| Alta | {high_conf} | {high_conf/total*100:.1f}% |\n")
|
||||
f.write(f"| Media | {medium_conf} | {medium_conf/total*100:.1f}% |\n")
|
||||
f.write(f"| No encontradas | {total-matched} | {(total-matched)/total*100:.1f}% |\n")
|
||||
|
||||
print(f"✅ Reporte detallado generado: {full_output_file}")
|
||||
|
||||
def main():
|
||||
print("🚀 Iniciando análisis detallado de adaptación de IOs TwinCAT ↔ TIA Portal")
|
||||
print("=" * 80)
|
||||
|
||||
# Cargar configuración
|
||||
configs = load_configuration()
|
||||
|
||||
# Verificar que se cargó correctamente
|
||||
if not configs:
|
||||
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
|
||||
working_directory = "./"
|
||||
else:
|
||||
working_directory = configs.get("working_directory", "./")
|
||||
|
||||
# Verificar directorio de trabajo
|
||||
if not os.path.exists(working_directory):
|
||||
print(f"Error: El directorio de trabajo no existe: {working_directory}")
|
||||
return
|
||||
|
||||
print(f"📁 Directorio de trabajo: {working_directory}")
|
||||
|
||||
# Crear directorio de resultados
|
||||
results_dir = create_results_directory(working_directory)
|
||||
|
||||
# Cargar datos
|
||||
tia_adaptations = load_tiaportal_adaptations(working_directory)
|
||||
twincat_definitions = scan_twincat_definitions(working_directory)
|
||||
twincat_usage = scan_twincat_usage(working_directory)
|
||||
|
||||
# Analizar correlaciones
|
||||
results = analyze_adaptations(tia_adaptations, twincat_definitions, twincat_usage)
|
||||
|
||||
# Generar reportes en el directorio de resultados
|
||||
generate_detailed_report(results, working_directory)
|
||||
generate_json_output(results, working_directory)
|
||||
|
||||
# Generar CSV para análisis adicional
|
||||
df = pd.DataFrame(results)
|
||||
csv_file = results_dir / 'io_detailed_analysis.csv'
|
||||
df.to_csv(csv_file, index=False, encoding='utf-8')
|
||||
print(f"✅ Datos exportados a CSV: {csv_file}")
|
||||
|
||||
print(f"\n🎉 Análisis completado exitosamente!")
|
||||
print(f"📁 Archivos generados en: {results_dir.absolute()}")
|
||||
print(f" 📄 {results_dir / 'IO_Detailed_Analysis_Report.md'}")
|
||||
print(f" 📄 {results_dir / 'io_adaptation_data.json'}")
|
||||
print(f" 📄 {results_dir / 'io_detailed_analysis.csv'}")
|
||||
|
||||
return results
|
||||
|
||||
if __name__ == "__main__":
|
||||
results = main()
|
|
@ -1,115 +0,0 @@
|
|||
# Análisis de Adaptación IO - TwinCAT ↔ TIA Portal
|
||||
|
||||
Scripts de análisis automático para correlacionar variables IO entre plataformas TwinCAT y TIA Portal en el proyecto SIDEL E5.007560.
|
||||
|
||||
## 📋 Descripción General
|
||||
|
||||
Este proyecto automatiza el análisis de adaptación de variables de entrada/salida (IO) entre:
|
||||
- **TIA Portal** (Siemens) - Sistema actual
|
||||
- **TwinCAT** (Beckhoff) - Sistema objetivo de migración
|
||||
|
||||
## 🔧 Scripts Incluidos
|
||||
|
||||
### 1. `x1_io_adaptation_script.py` - Análisis de Correlación IO
|
||||
|
||||
**Propósito:** Encuentra y correlaciona variables IO entre ambas plataformas generando reportes detallados.
|
||||
|
||||
**Archivos requeridos:**
|
||||
- `IO Adapted.md` - Tabla de adaptaciones TIA Portal (debe estar en directorio raíz)
|
||||
- `TwinCat/` - Directorio con archivos `.scl` de TwinCAT
|
||||
- `TiaPortal/` - Directorio con archivos `.md` de TIA Portal
|
||||
|
||||
**Archivos generados:**
|
||||
- `resultados/IO_Detailed_Analysis_Report.md` - Reporte con tablas markdown
|
||||
- `resultados/io_adaptation_data.json` - Datos estructurados para análisis
|
||||
- `resultados/io_detailed_analysis.csv` - Datos tabulares
|
||||
|
||||
### 2. `x2_code_snippets_generator.py` - Generador de Snippets de Código
|
||||
|
||||
**Propósito:** Genera snippets de código mostrando el uso real de cada variable en ambas plataformas.
|
||||
|
||||
**Archivos requeridos:**
|
||||
- `resultados/io_adaptation_data.json` - Generado por el script 1
|
||||
- `TwinCat/` - Directorio con archivos `.scl`
|
||||
- `TiaPortal/` - Directorio con archivos `.md`
|
||||
|
||||
**Archivos generados:**
|
||||
- `resultados/IO_Code_Snippets_Report.md` - Snippets de código con contexto
|
||||
- `resultados/IO_Usage_Statistics.md` - Estadísticas de uso
|
||||
|
||||
## 🚀 Uso
|
||||
|
||||
### Paso 1: Ejecutar análisis de correlación
|
||||
```bash
|
||||
python x1_io_adaptation_script.py
|
||||
```
|
||||
|
||||
### Paso 2: Generar snippets de código
|
||||
```bash
|
||||
python x2_code_snippets_generator.py
|
||||
```
|
||||
|
||||
## 📁 Estructura de Directorios Requerida
|
||||
|
||||
```
|
||||
proyecto/
|
||||
├── x1_io_adaptation_script.py
|
||||
├── x2_code_snippets_generator.py
|
||||
├── IO Adapted.md # Tabla de adaptaciones TIA
|
||||
├── TwinCat/ # Archivos .scl TwinCAT
|
||||
│ ├── GLOBAL_VARIABLES_IN_OUT.scl
|
||||
│ ├── INPUT.scl
|
||||
│ └── ... (otros archivos .scl)
|
||||
├── TiaPortal/ # Archivos .md TIA Portal
|
||||
│ ├── Input.md
|
||||
│ ├── Output.md
|
||||
│ └── ... (otros archivos .md)
|
||||
└── resultados/ # Directorio creado automáticamente
|
||||
├── IO_Detailed_Analysis_Report.md
|
||||
├── io_adaptation_data.json
|
||||
├── io_detailed_analysis.csv
|
||||
├── IO_Code_Snippets_Report.md
|
||||
└── IO_Usage_Statistics.md
|
||||
```
|
||||
|
||||
## 🔍 Funcionalidades Principales
|
||||
|
||||
### Script 1 - Análisis de Correlación
|
||||
- ✅ Convierte direcciones TIA Portal a formato TwinCAT
|
||||
- ✅ Busca variables por dirección exacta y similitud de nombres
|
||||
- ✅ Calcula nivel de confianza de correlaciones
|
||||
- ✅ Genera reportes en múltiples formatos (MD, JSON, CSV)
|
||||
|
||||
### Script 2 - Snippets de Código
|
||||
- ✅ Muestra hasta 3 usos por variable por plataforma
|
||||
- ✅ Contexto de 3 líneas (anterior, actual, siguiente)
|
||||
- ✅ Links markdown a archivos fuente
|
||||
- ✅ Estadísticas de uso y archivos más referenciados
|
||||
|
||||
## 📊 Resultados Típicos
|
||||
|
||||
- **Variables procesadas:** ~90-100 adaptaciones IO
|
||||
- **Tasa de correlación:** ~70-80% de variables encontradas
|
||||
- **Confianza alta:** Correlaciones por dirección exacta
|
||||
- **Variable más usada:** Típicamente botones de reset/start/stop
|
||||
|
||||
## 🛠 Dependencias
|
||||
|
||||
```python
|
||||
pandas
|
||||
pathlib (incluida en Python 3.4+)
|
||||
json (incluida en Python estándar)
|
||||
re (incluida en Python estándar)
|
||||
```
|
||||
|
||||
## 📝 Notas Importantes
|
||||
|
||||
1. **Orden de ejecución:** Ejecutar siempre el Script 1 antes que el Script 2
|
||||
2. **Archivos fuente:** Verificar que existan los directorios TwinCat/ y TiaPortal/
|
||||
3. **Codificación:** Los scripts manejan archivos con encoding UTF-8
|
||||
4. **Rendimiento:** El Script 2 puede tardar algunos minutos procesando archivos grandes
|
||||
|
||||
## 👥 Proyecto
|
||||
|
||||
**Proyecto SIDEL:** E5.007560 - Modifica O&U - SAE235
|
||||
**Automatización:** Migración TIA Portal → TwinCAT
|
|
@ -1,315 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Script para generar snippets de código de uso de variables IO
|
||||
entre TwinCAT y TIA Portal - Proyecto SIDEL
|
||||
|
||||
Autor: Generado automáticamente
|
||||
Proyecto: E5.007560 - Modifica O&U - SAE235
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Tuple, Optional
|
||||
import pandas as pd
|
||||
|
||||
# Configurar el path al directorio raíz del proyecto
|
||||
script_root = os.path.dirname(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
||||
)
|
||||
sys.path.append(script_root)
|
||||
|
||||
# Importar la función de configuración
|
||||
from backend.script_utils import load_configuration
|
||||
|
||||
|
||||
def load_adaptation_data(working_directory, json_file='io_adaptation_data.json'):
|
||||
"""Carga los datos de adaptación desde el archivo JSON"""
|
||||
full_json_file = os.path.join(working_directory, 'resultados', json_file)
|
||||
print(f"📖 Cargando datos de adaptación desde: {full_json_file}")
|
||||
|
||||
if not os.path.exists(full_json_file):
|
||||
print(f"⚠️ Archivo {full_json_file} no encontrado")
|
||||
return None
|
||||
|
||||
with open(full_json_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
|
||||
print(f"✅ Cargados datos de {data['metadata']['total_adaptations']} adaptaciones")
|
||||
return data
|
||||
|
||||
|
||||
def find_variable_usage_in_file(file_path, variable_name, max_occurrences=3):
|
||||
"""Encuentra el uso de una variable en un archivo específico y retorna el contexto"""
|
||||
if not os.path.exists(file_path):
|
||||
return []
|
||||
|
||||
usages = []
|
||||
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Buscar todas las líneas que contienen la variable
|
||||
found_lines = []
|
||||
for line_num, line in enumerate(lines):
|
||||
# Buscar la variable como palabra completa (no como parte de otra palabra)
|
||||
if re.search(rf'\b{re.escape(variable_name)}\b', line):
|
||||
found_lines.append((line_num, line.strip()))
|
||||
if len(found_lines) >= max_occurrences:
|
||||
break
|
||||
|
||||
# Para cada ocurrencia, obtener contexto (línea anterior, actual, siguiente)
|
||||
for line_num, line_content in found_lines:
|
||||
context = {
|
||||
'line_number': line_num + 1, # Convertir a 1-indexado
|
||||
'before': lines[line_num - 1].strip() if line_num > 0 else "",
|
||||
'current': line_content,
|
||||
'after': lines[line_num + 1].strip() if line_num < len(lines) - 1 else ""
|
||||
}
|
||||
usages.append(context)
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error leyendo archivo {file_path}: {e}")
|
||||
|
||||
return usages
|
||||
|
||||
|
||||
def find_tia_portal_usage(adaptation, working_directory):
|
||||
"""Busca el uso de variables TIA Portal en archivos markdown"""
|
||||
tia_address = adaptation['tia_portal']['address']
|
||||
tia_tag = adaptation['tia_portal']['tag']
|
||||
|
||||
# Buscar en archivos TIA Portal (principalmente en archivos .md)
|
||||
tia_usages = []
|
||||
|
||||
# Buscar en TiaPortal/ directory
|
||||
tia_portal_dir = Path(working_directory) / 'TiaPortal'
|
||||
if tia_portal_dir.exists():
|
||||
for md_file in tia_portal_dir.glob('*.md'):
|
||||
# Buscar por dirección TIA
|
||||
address_usages = find_variable_usage_in_file(md_file, tia_address, 2)
|
||||
for usage in address_usages:
|
||||
usage['file'] = f"TiaPortal/{md_file.name}"
|
||||
usage['search_term'] = tia_address
|
||||
tia_usages.append(usage)
|
||||
|
||||
# Buscar por tag TIA si es diferente
|
||||
if tia_tag != tia_address:
|
||||
tag_usages = find_variable_usage_in_file(md_file, tia_tag, 1)
|
||||
for usage in tag_usages:
|
||||
usage['file'] = f"TiaPortal/{md_file.name}"
|
||||
usage['search_term'] = tia_tag
|
||||
tia_usages.append(usage)
|
||||
|
||||
# Limitar total de usos TIA
|
||||
if len(tia_usages) >= 3:
|
||||
break
|
||||
|
||||
return tia_usages[:3] # Máximo 3 usos TIA
|
||||
|
||||
|
||||
def find_twincat_usage(adaptation, working_directory):
|
||||
"""Busca el uso de variables TwinCAT en archivos .scl"""
|
||||
if not adaptation['correlation']['found']:
|
||||
return []
|
||||
|
||||
variable_name = adaptation['twincat']['variable']
|
||||
usage_files = adaptation['usage']['usage_files']
|
||||
|
||||
twincat_usages = []
|
||||
|
||||
# Buscar en archivos TwinCAT
|
||||
twincat_dir = Path(working_directory) / 'TwinCat'
|
||||
if twincat_dir.exists():
|
||||
for file_name in usage_files:
|
||||
file_path = twincat_dir / file_name
|
||||
if file_path.exists():
|
||||
usages = find_variable_usage_in_file(file_path, variable_name, 2)
|
||||
for usage in usages:
|
||||
usage['file'] = f"TwinCat/{file_name}"
|
||||
usage['search_term'] = variable_name
|
||||
twincat_usages.append(usage)
|
||||
|
||||
# Limitar por archivo
|
||||
if len(twincat_usages) >= 3:
|
||||
break
|
||||
|
||||
return twincat_usages[:3] # Máximo 3 usos TwinCAT
|
||||
|
||||
|
||||
def generate_code_snippets_report(data, working_directory, output_file='IO_Code_Snippets_Report.md'):
|
||||
"""Genera el reporte con snippets de código"""
|
||||
full_output_file = os.path.join(working_directory, 'resultados', output_file)
|
||||
print(f"\n📄 Generando reporte de snippets: {full_output_file}")
|
||||
|
||||
matched_adaptations = [a for a in data['adaptations'] if a['correlation']['found']]
|
||||
|
||||
with open(full_output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("# Reporte de Snippets de Código - Adaptación IO\n\n")
|
||||
f.write(f"**Fecha de generación:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"**Proyecto:** {data['metadata']['project']}\n\n")
|
||||
|
||||
f.write("## 📋 Resumen\n\n")
|
||||
f.write(f"- **Variables analizadas:** {len(matched_adaptations)}\n")
|
||||
f.write(f"- **Snippets generados:** Se muestran hasta 3 usos por plataforma\n")
|
||||
f.write(f"- **Formato:** Contexto de 3 líneas (anterior, actual, siguiente)\n\n")
|
||||
|
||||
f.write("---\n\n")
|
||||
|
||||
# Procesar cada adaptación
|
||||
for i, adaptation in enumerate(matched_adaptations, 1):
|
||||
tia_address = adaptation['tia_portal']['address']
|
||||
tia_tag = adaptation['tia_portal']['tag']
|
||||
twincat_var = adaptation['twincat']['variable']
|
||||
twincat_addr = adaptation['twincat']['address']
|
||||
|
||||
print(f" 📝 Procesando {i}/{len(matched_adaptations)}: {tia_address} → {twincat_var}")
|
||||
|
||||
f.write(f"## {i}. {tia_address} → {twincat_var}\n\n")
|
||||
f.write(f"**TIA Portal:** `{tia_tag}` (`{tia_address}`)\n")
|
||||
f.write(f"**TwinCAT:** `{twincat_var}` (`%{twincat_addr}`)\n")
|
||||
f.write(f"**Tipo:** `{adaptation['twincat']['data_type']}`\n\n")
|
||||
|
||||
# Buscar usos en TIA Portal
|
||||
f.write("### 🔵 Uso en TIA Portal\n\n")
|
||||
tia_usages = find_tia_portal_usage(adaptation, working_directory)
|
||||
|
||||
if tia_usages:
|
||||
for j, usage in enumerate(tia_usages):
|
||||
f.write(f"**Uso {j+1}:** [{usage['file']}]({usage['file']}) - Línea {usage['line_number']}\n\n")
|
||||
f.write("```scl\n")
|
||||
if usage['before']:
|
||||
f.write(f"{usage['before']}\n")
|
||||
f.write(f">>> {usage['current']} // ← {usage['search_term']}\n")
|
||||
if usage['after']:
|
||||
f.write(f"{usage['after']}\n")
|
||||
f.write("```\n\n")
|
||||
else:
|
||||
f.write("*No se encontraron usos específicos en archivos TIA Portal.*\n\n")
|
||||
|
||||
# Buscar usos en TwinCAT
|
||||
f.write("### 🟢 Uso en TwinCAT\n\n")
|
||||
twincat_usages = find_twincat_usage(adaptation, working_directory)
|
||||
|
||||
if twincat_usages:
|
||||
for j, usage in enumerate(twincat_usages):
|
||||
f.write(f"**Uso {j+1}:** [{usage['file']}]({usage['file']}) - Línea {usage['line_number']}\n\n")
|
||||
f.write("```scl\n")
|
||||
if usage['before']:
|
||||
f.write(f"{usage['before']}\n")
|
||||
f.write(f">>> {usage['current']} // ← {usage['search_term']}\n")
|
||||
if usage['after']:
|
||||
f.write(f"{usage['after']}\n")
|
||||
f.write("```\n\n")
|
||||
else:
|
||||
f.write("*Variable definida pero no se encontraron usos específicos.*\n\n")
|
||||
|
||||
f.write("---\n\n")
|
||||
|
||||
print(f"✅ Reporte de snippets generado: {full_output_file}")
|
||||
|
||||
|
||||
def generate_summary_statistics(data, working_directory, output_file='IO_Usage_Statistics.md'):
|
||||
"""Genera estadísticas de uso de las variables"""
|
||||
full_output_file = os.path.join(working_directory, 'resultados', output_file)
|
||||
print(f"\n📊 Generando estadísticas de uso: {full_output_file}")
|
||||
|
||||
matched_adaptations = [a for a in data['adaptations'] if a['correlation']['found']]
|
||||
|
||||
# Calcular estadísticas
|
||||
total_usage = sum(a['usage']['usage_count'] for a in matched_adaptations)
|
||||
variables_with_usage = len([a for a in matched_adaptations if a['usage']['usage_count'] > 0])
|
||||
|
||||
# Variables más usadas
|
||||
most_used = sorted(matched_adaptations, key=lambda x: x['usage']['usage_count'], reverse=True)[:10]
|
||||
|
||||
# Archivos más referenciados
|
||||
file_usage = {}
|
||||
for adaptation in matched_adaptations:
|
||||
for file_name in adaptation['usage']['usage_files']:
|
||||
file_usage[file_name] = file_usage.get(file_name, 0) + 1
|
||||
|
||||
top_files = sorted(file_usage.items(), key=lambda x: x[1], reverse=True)[:10]
|
||||
|
||||
with open(full_output_file, 'w', encoding='utf-8') as f:
|
||||
f.write("# Estadísticas de Uso de Variables IO\n\n")
|
||||
f.write(f"**Fecha de generación:** {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
||||
|
||||
f.write("## 📊 Resumen General\n\n")
|
||||
f.write(f"- **Variables correlacionadas:** {len(matched_adaptations)}\n")
|
||||
f.write(f"- **Variables con uso documentado:** {variables_with_usage}\n")
|
||||
f.write(f"- **Total de usos encontrados:** {total_usage}\n")
|
||||
f.write(f"- **Promedio de usos por variable:** {total_usage/len(matched_adaptations):.1f}\n\n")
|
||||
|
||||
f.write("## 🔥 Top 10 Variables Más Usadas\n\n")
|
||||
f.write("| Ranking | TIA Address | TwinCAT Variable | Usos | Archivos |\n")
|
||||
f.write("|---------|-------------|------------------|------|----------|\n")
|
||||
|
||||
for i, adaptation in enumerate(most_used, 1):
|
||||
files_str = ', '.join(adaptation['usage']['usage_files'][:3])
|
||||
if len(adaptation['usage']['usage_files']) > 3:
|
||||
files_str += '...'
|
||||
|
||||
f.write(f"| {i} | {adaptation['tia_portal']['address']} | "
|
||||
f"`{adaptation['twincat']['variable']}` | "
|
||||
f"{adaptation['usage']['usage_count']} | {files_str} |\n")
|
||||
|
||||
f.write("\n## 📁 Top 10 Archivos Más Referenciados\n\n")
|
||||
f.write("| Ranking | Archivo | Variables Usadas |\n")
|
||||
f.write("|---------|---------|------------------|\n")
|
||||
|
||||
for i, (file_name, count) in enumerate(top_files, 1):
|
||||
f.write(f"| {i} | `{file_name}` | {count} |\n")
|
||||
|
||||
print(f"✅ Estadísticas de uso generadas: {full_output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
print("🚀 Iniciando generación de snippets de código para adaptación IO")
|
||||
print("=" * 70)
|
||||
|
||||
# Cargar configuración
|
||||
configs = load_configuration()
|
||||
|
||||
# Verificar que se cargó correctamente
|
||||
if not configs:
|
||||
print("Advertencia: No se pudo cargar la configuración, usando valores por defecto")
|
||||
working_directory = "./"
|
||||
else:
|
||||
working_directory = configs.get("working_directory", "./")
|
||||
|
||||
# Verificar directorio de trabajo
|
||||
if not os.path.exists(working_directory):
|
||||
print(f"Error: El directorio de trabajo no existe: {working_directory}")
|
||||
return
|
||||
|
||||
print(f"📁 Directorio de trabajo: {working_directory}")
|
||||
|
||||
# Crear directorio de resultados si no existe
|
||||
results_dir = Path(working_directory) / 'resultados'
|
||||
results_dir.mkdir(exist_ok=True)
|
||||
|
||||
# Cargar datos de adaptación
|
||||
data = load_adaptation_data(working_directory)
|
||||
if not data:
|
||||
print("❌ No se pudieron cargar los datos de adaptación")
|
||||
return
|
||||
|
||||
# Generar reporte de snippets
|
||||
generate_code_snippets_report(data, working_directory)
|
||||
|
||||
# Generar estadísticas de uso
|
||||
generate_summary_statistics(data, working_directory)
|
||||
|
||||
print(f"\n🎉 Generación completada exitosamente!")
|
||||
print(f"📁 Archivos generados en: {results_dir.absolute()}")
|
||||
print(f" 📄 {results_dir / 'IO_Code_Snippets_Report.md'}")
|
||||
print(f" 📄 {results_dir / 'IO_Usage_Statistics.md'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
File diff suppressed because it is too large
Load Diff
|
@ -4,7 +4,13 @@
|
|||
"path": "."
|
||||
},
|
||||
{
|
||||
"path": "C:/Trabajo/SIDEL/13 - E5.007560 - Modifica O&U - SAE235/Reporte/ExportTia"
|
||||
"path": "C:/Program Files/Siemens/Automation/Portal V19/PublicAPI/V19/Schemas"
|
||||
},
|
||||
{
|
||||
"path": "../../../../../../Trabajo/VM/44 - 98050 - Fiera/Reporte/ExportsTia/Source/98050_PLC"
|
||||
},
|
||||
{
|
||||
"path": "../../../../../../Trabajo/VM/22 - 93841 - Sidel - Tilting/Reporte/TiaExports"
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,31 +1,90 @@
|
|||
--- Log de Ejecución: x7_clear.py ---
|
||||
Grupo: XML Parser to SCL
|
||||
Directorio de Trabajo: C:\Trabajo\SIDEL\13 - E5.007560 - Modifica O&U - SAE235\Reporte\ExportTia
|
||||
Inicio: 2025-06-20 18:53:46
|
||||
Fin: 2025-06-20 18:53:47
|
||||
Duración: 0:00:01.131243
|
||||
Directorio de Trabajo: D:\Trabajo\VM\44 - 98050 - Fiera\Reporte\ExportsTia\Source
|
||||
Inicio: 2025-06-13 01:01:10
|
||||
Fin: 2025-06-13 01:01:11
|
||||
Duración: 0:00:00.701052
|
||||
Estado: SUCCESS (Código de Salida: 0)
|
||||
|
||||
--- SALIDA ESTÁNDAR (STDOUT) ---
|
||||
INFO: format_variable_name importado desde generators.generator_utils
|
||||
|
||||
=== Limpiando PLC: PLC ===
|
||||
- Eliminado directorio de parsing: PLC\PlcDataTypes\parsing
|
||||
- Eliminado directorio de parsing: PLC\PlcDataTypes_CR\parsing
|
||||
- Eliminado directorio de parsing: PLC\PlcTags\parsing
|
||||
- Eliminado directorio de parsing: PLC\PlcTags\IO Not in Hardware\parsing
|
||||
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\parsing
|
||||
- Eliminado directorio de parsing: PLC\ProgramBlocks_CR\40_10_GNS_PLCdia Main\parsing
|
||||
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\parsing
|
||||
- Eliminado directorio de parsing: PLC\ProgramBlocks_XML\40_10_GNS_PLCdia Main\parsing
|
||||
- Eliminado directorio de parsing: PLC\SystemBlocks_CR\parsing
|
||||
- Eliminado directorio 'scl_output': PLC\scl_output
|
||||
- Eliminado directorio 'xref_output': PLC\xref_output
|
||||
- Eliminado archivo agregado: PLC\full_project_representation.md
|
||||
- Eliminado log: log_PLC.txt
|
||||
=== Limpiando PLC: 98050_PLC ===
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\MiniMotor\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\MiniMotor\DBS55_PN_Extend-A\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\SICK AG\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\CONVEYORS\TRANSFER\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\ConveyorsBase\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\Siemens\LCamHdl_Types\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\Motion\Technology\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\SeamlessDivider\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Library\SeamlessDivider\Technology\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Machine\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcDataTypes\Machine\Cycle\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcTags\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\PlcTags\Library\Motion\Siemens\LCamHdl_Tags\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\1-AIR Philosophy\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\2-TTOP Philosophy\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\MiniMotor_PN\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\3-Motors Manage\MiniMotor_PN\MiniMotor_PN\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\HMI\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\FC\MACHINE SIGNALS\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!! SYS !!!\OB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\!!!TRANSFER\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\FC\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\0 - MAIN\OB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\FB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Device\FC\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\General\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\General\FC\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\DB\Minimotor\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\FC\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\2 - TTOP\Motor\FC\Minimotor\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\FB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\1 - CONVEYORS\4 - LUBE\FB\OLD\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\2 - MACHINE\DB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\2 - MACHINE\FB\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\Hmi\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\System\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\! ConveyorsSTD\TimeZone\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\AAA_Debug\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\AAA_VirtualMaster\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\ExchangeSignals\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\ExchangeSignals\Loop\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\HMI\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Instances\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Generic\Alarms\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Siemens\LCamHdl_Blocks\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Technology\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\Motion\Utilities\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\SeamlessDivider\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Libraries\SeamlessDivider\Technology\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Machine\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Machine\Instances\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\Setup\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\TimingBelt (downstream divider)\parsing
|
||||
- Eliminado directorio de parsing: 98050_PLC\ProgramBlocks_XML\Divider\TimingBelt (downstream divider)\Instances\parsing
|
||||
- Eliminado directorio 'scl_output': 98050_PLC\scl_output
|
||||
- Eliminado directorio 'xref_output': 98050_PLC\xref_output
|
||||
- Eliminado archivo agregado: 98050_PLC\full_project_representation.md
|
||||
- Eliminado log: log_98050_PLC.txt
|
||||
|
||||
--- Resumen de limpieza ---
|
||||
Directorios eliminados: 11
|
||||
Directorios eliminados: 70
|
||||
Archivos eliminados: 2
|
||||
Limpieza completada.
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -273,25 +273,6 @@ def parse_lad_fbd_network(network_element):
|
|||
# --- Poblar Entradas ---
|
||||
# Lista base de pines posibles (podría obtenerse de XSDs o dinámicamente)
|
||||
possible_input_pins = set(["en", "in", "in1", "in2", "pre"])
|
||||
|
||||
# **NUEVO: Generar pines dinámicamente para compuertas OR/AND basándose en Cardinality**
|
||||
if original_type in ["O", "And"]: # Compuertas lógicas
|
||||
cardinality = instruction_info.get("template_values", {}).get("Card")
|
||||
if cardinality:
|
||||
try:
|
||||
num_inputs = int(cardinality)
|
||||
# Generar pines in1, in2, ..., inN
|
||||
for i in range(1, num_inputs + 1):
|
||||
possible_input_pins.add(f"in{i}")
|
||||
print(f"INFO: Compuerta {original_type} UID {instruction_uid} con cardinalidad {num_inputs} - generando pines in1...in{num_inputs}")
|
||||
except (ValueError, TypeError):
|
||||
print(f"Advertencia: Cardinalidad inválida '{cardinality}' para {original_type} UID {instruction_uid}")
|
||||
# Fallback a pines estándar
|
||||
possible_input_pins.update(["in1", "in2"])
|
||||
else:
|
||||
# Sin cardinalidad explícita, usar pines estándar
|
||||
possible_input_pins.update(["in1", "in2"])
|
||||
|
||||
# Añadir pines dinámicamente basados en el tipo de instrucción
|
||||
if original_type in ["Contact", "Coil", "SCoil", "RCoil", "SdCoil"]:
|
||||
possible_input_pins.add("operand")
|
||||
|
|
|
@ -276,16 +276,15 @@ def parse_part(part_element):
|
|||
template_values = {}
|
||||
negated_pins = {}
|
||||
try:
|
||||
for tv in part_element.xpath("./flg:TemplateValue", namespaces=ns):
|
||||
for tv in part_element.xpath("./TemplateValue"):
|
||||
tv_name = tv.get("Name")
|
||||
tv_type = tv.get("Type")
|
||||
tv_value = tv.text.strip() if tv.text else tv_type # Obtener valor real del elemento
|
||||
if tv_name:
|
||||
template_values[tv_name] = tv_value
|
||||
if tv_name and tv_type:
|
||||
template_values[tv_name] = tv_type
|
||||
except Exception as e:
|
||||
print(f"Advertencia: Error extrayendo TemplateValues Part UID={uid}: {e}")
|
||||
try:
|
||||
for negated_elem in part_element.xpath("./flg:Negated", namespaces=ns):
|
||||
for negated_elem in part_element.xpath("./Negated"):
|
||||
negated_pin_name = negated_elem.get("Name")
|
||||
if negated_pin_name:
|
||||
negated_pins[negated_pin_name] = True
|
||||
|
|
|
@ -15,5 +15,5 @@
|
|||
"xref_source_subdir": "source"
|
||||
},
|
||||
"level3": {},
|
||||
"working_directory": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia"
|
||||
"working_directory": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source"
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"path": "C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||
"path": "D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||
"history": [
|
||||
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||
"D:\\Trabajo\\VM\\44 - 98050 - Fiera\\Reporte\\ExportsTia\\Source",
|
||||
"C:\\Trabajo\\SIDEL\\13 - E5.007560 - Modifica O&U - SAE235\\Reporte\\ExportTia",
|
||||
"D:\\Trabajo\\VM\\22 - 93841 - Sidel - Tilting\\Reporte\\TiaExports",
|
||||
"C:\\Trabajo\\SIDEL\\09 - SAE452 - Diet as Regular - San Giovanni in Bosco\\Reporte\\SourceDoc\\SourceXML",
|
||||
"C:\\Trabajo\\SIDEL\\06 - E5.007363 - Modifica O&U - SAE196 (cip integrato)\\Reporte\\IOExport"
|
||||
|
|
43711
data/log.txt
43711
data/log.txt
File diff suppressed because it is too large
Load Diff
|
@ -1,900 +0,0 @@
|
|||
import os
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
class PythonLauncherManager:
|
||||
def __init__(self, data_path: str):
|
||||
self.data_path = data_path
|
||||
self.launcher_config_path = os.path.join(data_path, "python_launcher_projects.json")
|
||||
self.favorites_path = os.path.join(data_path, "python_launcher_favorites.json")
|
||||
self.history_path = os.path.join(data_path, "python_launcher_history.json")
|
||||
self.script_metadata_path = os.path.join(data_path, "python_launcher_script_metadata.json")
|
||||
|
||||
# Procesos en ejecución para Python (servidores, etc.)
|
||||
self.running_processes = {}
|
||||
self.process_lock = threading.Lock()
|
||||
|
||||
# Inicializar archivos si no existen
|
||||
self._initialize_files()
|
||||
|
||||
def _initialize_files(self):
|
||||
"""Crear archivos de configuración por defecto si no existen"""
|
||||
# Inicializar python_launcher_projects.json
|
||||
if not os.path.exists(self.launcher_config_path):
|
||||
default_config = {
|
||||
"version": "1.0",
|
||||
"projects": [],
|
||||
"categories": {
|
||||
"MCP Servers": {
|
||||
"color": "#3B82F6",
|
||||
"icon": "🔌",
|
||||
"subcategories": ["Anthropic", "Custom", "OpenAI"]
|
||||
},
|
||||
"Flask Apps": {
|
||||
"color": "#10B981",
|
||||
"icon": "🌐",
|
||||
"subcategories": ["API", "Web App", "Microservice"]
|
||||
},
|
||||
"Scripts": {
|
||||
"color": "#8B5CF6",
|
||||
"icon": "📜",
|
||||
"subcategories": ["Automatización", "Utiles", "Procesamiento"]
|
||||
},
|
||||
"Bots": {
|
||||
"color": "#F59E0B",
|
||||
"icon": "🤖",
|
||||
"subcategories": ["Discord", "Telegram", "Slack"]
|
||||
},
|
||||
"Data Processing": {
|
||||
"color": "#EF4444",
|
||||
"icon": "📊",
|
||||
"subcategories": ["ETL", "Analysis", "ML"]
|
||||
},
|
||||
"Otros": {
|
||||
"color": "#6B7280",
|
||||
"icon": "📁",
|
||||
"subcategories": ["Misceláneos"]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"default_execution_directory": "project_directory",
|
||||
"enable_argument_validation": True,
|
||||
"max_history_entries": 100,
|
||||
"auto_cleanup_days": 30,
|
||||
"default_python_env": "base"
|
||||
}
|
||||
}
|
||||
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Inicializar python_launcher_favorites.json
|
||||
if not os.path.exists(self.favorites_path):
|
||||
default_favorites = {"favorites": []}
|
||||
with open(self.favorites_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_favorites, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Inicializar python_launcher_history.json
|
||||
if not os.path.exists(self.history_path):
|
||||
default_history = {
|
||||
"history": [],
|
||||
"settings": {
|
||||
"max_entries": 100,
|
||||
"auto_cleanup_days": 30,
|
||||
"track_execution_time": True,
|
||||
"track_arguments": True
|
||||
}
|
||||
}
|
||||
with open(self.history_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_history, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Inicializar python_launcher_script_metadata.json
|
||||
if not os.path.exists(self.script_metadata_path):
|
||||
default_metadata = {
|
||||
"version": "1.0",
|
||||
"script_metadata": {}
|
||||
}
|
||||
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(default_metadata, f, indent=2, ensure_ascii=False)
|
||||
|
||||
def get_python_projects(self) -> List[Dict[str, Any]]:
|
||||
"""Obtener todos los proyectos Python"""
|
||||
try:
|
||||
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
return config.get("projects", [])
|
||||
except Exception as e:
|
||||
print(f"Error loading Python projects: {e}")
|
||||
return []
|
||||
|
||||
def get_python_project(self, project_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Obtener un proyecto específico por ID"""
|
||||
projects = self.get_python_projects()
|
||||
for project in projects:
|
||||
if project.get("id") == project_id:
|
||||
return project
|
||||
return None
|
||||
|
||||
def add_python_project(self, project_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Agregar nuevo proyecto Python"""
|
||||
try:
|
||||
# Validar datos requeridos
|
||||
required_fields = ["name", "directory"]
|
||||
for field in required_fields:
|
||||
if not project_data.get(field):
|
||||
return {"status": "error", "message": f"Campo requerido: {field}"}
|
||||
|
||||
# Validar que el directorio existe
|
||||
if not os.path.isdir(project_data["directory"]):
|
||||
return {"status": "error", "message": "El directorio especificado no existe"}
|
||||
|
||||
# Generar ID único si no se proporciona
|
||||
if not project_data.get("id"):
|
||||
project_data["id"] = str(uuid.uuid4())[:8]
|
||||
|
||||
# Verificar que el ID no exista
|
||||
if self.get_python_project(project_data["id"]):
|
||||
return {"status": "error", "message": "Ya existe un proyecto con este ID"}
|
||||
|
||||
# Agregar campos por defecto
|
||||
current_time = datetime.now().isoformat() + "Z"
|
||||
project_data.setdefault("description", "")
|
||||
project_data.setdefault("category", "Otros")
|
||||
project_data.setdefault("version", "1.0")
|
||||
project_data.setdefault("author", "")
|
||||
project_data.setdefault("tags", [])
|
||||
project_data.setdefault("python_env", "base") # Entorno Python por defecto
|
||||
project_data.setdefault("created_date", current_time)
|
||||
project_data["updated_date"] = current_time
|
||||
|
||||
# Cargar configuración y agregar proyecto
|
||||
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
config["projects"].append(project_data)
|
||||
|
||||
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"status": "success", "message": "Proyecto agregado exitosamente"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error agregando proyecto: {str(e)}"}
|
||||
|
||||
def update_python_project(self, project_id: str, project_data: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Actualizar proyecto existente"""
|
||||
try:
|
||||
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Buscar y actualizar el proyecto
|
||||
project_found = False
|
||||
for i, project in enumerate(config["projects"]):
|
||||
if project["id"] == project_id:
|
||||
# Mantener ID y fechas de creación
|
||||
project_data["id"] = project_id
|
||||
project_data["created_date"] = project.get("created_date", datetime.now().isoformat() + "Z")
|
||||
project_data["updated_date"] = datetime.now().isoformat() + "Z"
|
||||
|
||||
config["projects"][i] = project_data
|
||||
project_found = True
|
||||
break
|
||||
|
||||
if not project_found:
|
||||
return {"status": "error", "message": "Proyecto no encontrado"}
|
||||
|
||||
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"status": "success", "message": "Proyecto actualizado exitosamente"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error actualizando proyecto: {str(e)}"}
|
||||
|
||||
def delete_python_project(self, project_id: str) -> Dict[str, str]:
|
||||
"""Eliminar proyecto Python"""
|
||||
try:
|
||||
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Filtrar el proyecto a eliminar
|
||||
original_count = len(config["projects"])
|
||||
config["projects"] = [p for p in config["projects"] if p["id"] != project_id]
|
||||
|
||||
if len(config["projects"]) == original_count:
|
||||
return {"status": "error", "message": "Proyecto no encontrado"}
|
||||
|
||||
with open(self.launcher_config_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||
|
||||
# Limpiar metadatos y favoritos relacionados
|
||||
self._cleanup_script_metadata_for_project(project_id)
|
||||
self._cleanup_favorites_for_project(project_id)
|
||||
|
||||
return {"status": "success", "message": "Proyecto eliminado exitosamente"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error eliminando proyecto: {str(e)}"}
|
||||
|
||||
def get_project_scripts(self, project_id: str) -> List[Dict[str, Any]]:
|
||||
"""Obtener scripts de un proyecto (solo .py visibles)"""
|
||||
project = self.get_python_project(project_id)
|
||||
if not project:
|
||||
return []
|
||||
|
||||
project_dir = project["directory"]
|
||||
if not os.path.isdir(project_dir):
|
||||
return []
|
||||
|
||||
scripts = []
|
||||
script_metadata = self._load_script_metadata()
|
||||
|
||||
# Buscar archivos .py en el directorio del proyecto
|
||||
for filename in os.listdir(project_dir):
|
||||
if filename.endswith('.py') and not filename.startswith('__'):
|
||||
script_path = os.path.join(project_dir, filename)
|
||||
if os.path.isfile(script_path):
|
||||
# Obtener metadatos del script
|
||||
metadata_key = f"{project_id}:{filename}"
|
||||
metadata = script_metadata.get("script_metadata", {}).get(metadata_key, {})
|
||||
|
||||
# Solo mostrar scripts no ocultos
|
||||
if not metadata.get("hidden", False):
|
||||
scripts.append({
|
||||
"filename": filename,
|
||||
"display_name": metadata.get("display_name", filename.replace('.py', '')),
|
||||
"description": metadata.get("description", ""),
|
||||
"tags": metadata.get("tags", []),
|
||||
"arguments": metadata.get("arguments", []),
|
||||
"is_server": metadata.get("is_server", False), # Indica si es un servidor que corre en background
|
||||
"server_port": metadata.get("server_port", ""),
|
||||
"requires_background": metadata.get("requires_background", False)
|
||||
})
|
||||
|
||||
return sorted(scripts, key=lambda x: x["display_name"])
|
||||
|
||||
def get_all_project_scripts(self, project_id: str) -> List[Dict[str, Any]]:
|
||||
"""Obtener TODOS los scripts de un proyecto (incluyendo ocultos) para gestión"""
|
||||
project = self.get_python_project(project_id)
|
||||
if not project:
|
||||
return []
|
||||
|
||||
project_dir = project["directory"]
|
||||
if not os.path.isdir(project_dir):
|
||||
return []
|
||||
|
||||
scripts = []
|
||||
script_metadata = self._load_script_metadata()
|
||||
|
||||
# Buscar archivos .py en el directorio del proyecto
|
||||
for filename in os.listdir(project_dir):
|
||||
if filename.endswith('.py') and not filename.startswith('__'):
|
||||
script_path = os.path.join(project_dir, filename)
|
||||
if os.path.isfile(script_path):
|
||||
# Obtener metadatos del script
|
||||
metadata_key = f"{project_id}:{filename}"
|
||||
metadata = script_metadata.get("script_metadata", {}).get(metadata_key, {})
|
||||
|
||||
scripts.append({
|
||||
"filename": filename,
|
||||
"display_name": metadata.get("display_name", filename.replace('.py', '')),
|
||||
"description": metadata.get("description", ""),
|
||||
"tags": metadata.get("tags", []),
|
||||
"arguments": metadata.get("arguments", []),
|
||||
"hidden": metadata.get("hidden", False),
|
||||
"is_server": metadata.get("is_server", False),
|
||||
"server_port": metadata.get("server_port", ""),
|
||||
"requires_background": metadata.get("requires_background", False)
|
||||
})
|
||||
|
||||
return sorted(scripts, key=lambda x: x["display_name"])
|
||||
|
||||
def get_script_metadata(self, project_id: str, script_name: str) -> Dict[str, Any]:
|
||||
"""Obtener metadatos de un script específico"""
|
||||
script_metadata = self._load_script_metadata()
|
||||
metadata_key = f"{project_id}:{script_name}"
|
||||
return script_metadata.get("script_metadata", {}).get(metadata_key, {})
|
||||
|
||||
def update_script_metadata(self, project_id: str, script_name: str, metadata: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""Actualizar metadatos de un script"""
|
||||
try:
|
||||
script_metadata = self._load_script_metadata()
|
||||
metadata_key = f"{project_id}:{script_name}"
|
||||
|
||||
if "script_metadata" not in script_metadata:
|
||||
script_metadata["script_metadata"] = {}
|
||||
|
||||
script_metadata["script_metadata"][metadata_key] = metadata
|
||||
self._save_script_metadata(script_metadata)
|
||||
|
||||
return {"status": "success", "message": "Metadatos actualizados exitosamente"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error actualizando metadatos: {str(e)}"}
|
||||
|
||||
def get_available_python_envs(self) -> List[Dict[str, str]]:
|
||||
"""Obtener lista de entornos de Python/Miniconda disponibles"""
|
||||
try:
|
||||
envs = [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
|
||||
|
||||
# Intentar encontrar Miniconda
|
||||
miniconda_paths = [
|
||||
r"C:\Users\migue\miniconda3",
|
||||
r"C:\ProgramData\miniconda3",
|
||||
r"C:\miniconda3",
|
||||
os.path.expanduser("~/miniconda3"),
|
||||
os.path.expanduser("~/anaconda3")
|
||||
]
|
||||
|
||||
for base_path in miniconda_paths:
|
||||
if os.path.exists(base_path):
|
||||
envs_path = os.path.join(base_path, "envs")
|
||||
if os.path.exists(envs_path):
|
||||
for env_name in os.listdir(envs_path):
|
||||
env_path = os.path.join(envs_path, env_name)
|
||||
python_exe = os.path.join(env_path, "python.exe")
|
||||
if os.path.exists(python_exe):
|
||||
envs.append({
|
||||
"name": env_name,
|
||||
"display_name": f"{env_name} (Miniconda)",
|
||||
"path": python_exe
|
||||
})
|
||||
break # Solo usar el primer Miniconda encontrado
|
||||
|
||||
return envs
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting Python environments: {e}")
|
||||
return [{"name": "base", "display_name": "Base (Sistema)", "path": sys.executable}]
|
||||
|
||||
def execute_python_script(self, project_id: str, script_name: str, script_args: List[str],
|
||||
broadcast_func, working_dir: str = None, run_in_background: bool = False) -> Dict[str, Any]:
|
||||
"""Ejecutar script Python con argumentos opcionales"""
|
||||
try:
|
||||
project = self.get_python_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Proyecto no encontrado"}
|
||||
|
||||
# Construir ruta del script
|
||||
script_path = os.path.join(project["directory"], script_name)
|
||||
if not os.path.exists(script_path):
|
||||
return {"error": f"Script '{script_name}' no encontrado"}
|
||||
|
||||
# Determinar directorio de trabajo
|
||||
if working_dir and os.path.isdir(working_dir):
|
||||
work_dir = working_dir
|
||||
else:
|
||||
work_dir = project["directory"]
|
||||
|
||||
# Obtener ejecutable de Python
|
||||
python_env = project.get("python_env", "base")
|
||||
python_exe = self._get_python_executable(python_env)
|
||||
|
||||
# Construir comando
|
||||
cmd = [python_exe, script_path] + script_args
|
||||
|
||||
# ID único para esta ejecución
|
||||
execution_id = str(uuid.uuid4())[:8]
|
||||
start_time = time.time()
|
||||
|
||||
broadcast_func(f"🚀 Ejecutando script: {script_name}")
|
||||
broadcast_func(f"📁 Directorio: {work_dir}")
|
||||
broadcast_func(f"🐍 Python: {python_exe}")
|
||||
if script_args:
|
||||
broadcast_func(f"⚙️ Argumentos: {' '.join(script_args)}")
|
||||
|
||||
# Agregar a historial
|
||||
history_entry = {
|
||||
"id": execution_id,
|
||||
"project_id": project_id,
|
||||
"script_name": script_name,
|
||||
"arguments": script_args,
|
||||
"working_directory": work_dir,
|
||||
"python_env": python_env,
|
||||
"timestamp": datetime.now().isoformat() + "Z",
|
||||
"status": "running",
|
||||
"execution_time": None
|
||||
}
|
||||
self._add_to_history(history_entry)
|
||||
|
||||
# Configurar proceso
|
||||
if sys.platform == "win32":
|
||||
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
if run_in_background:
|
||||
# Para procesos en background (servidores), crear ventana nueva
|
||||
creationflags |= subprocess.CREATE_NEW_CONSOLE
|
||||
else:
|
||||
creationflags = 0
|
||||
|
||||
# Ejecutar proceso
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
cwd=work_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
universal_newlines=True,
|
||||
creationflags=creationflags if sys.platform == "win32" else None
|
||||
)
|
||||
|
||||
# Guardar proceso en la lista de procesos activos
|
||||
with self.process_lock:
|
||||
self.running_processes[process.pid] = {
|
||||
"pid": process.pid,
|
||||
"project_id": project_id,
|
||||
"script_name": script_name,
|
||||
"start_time": datetime.now().isoformat() + "Z",
|
||||
"execution_id": execution_id,
|
||||
"working_directory": work_dir,
|
||||
"is_background": run_in_background
|
||||
}
|
||||
|
||||
broadcast_func(f"✅ Proceso iniciado con PID: {process.pid}")
|
||||
|
||||
if run_in_background:
|
||||
# Para procesos en background, no esperamos la salida
|
||||
broadcast_func(f"🔄 Script ejecutándose en segundo plano (PID: {process.pid})")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Script '{script_name}' iniciado en segundo plano",
|
||||
"execution_id": execution_id,
|
||||
"pid": process.pid,
|
||||
"background": True
|
||||
}
|
||||
else:
|
||||
# Para scripts normales, leer salida en tiempo real
|
||||
def read_output():
|
||||
try:
|
||||
for line in iter(process.stdout.readline, ''):
|
||||
if line:
|
||||
broadcast_func(line.rstrip())
|
||||
except Exception as e:
|
||||
broadcast_func(f"Error leyendo salida: {e}")
|
||||
finally:
|
||||
if process.stdout:
|
||||
process.stdout.close()
|
||||
|
||||
# Iniciar lectura de salida en hilo separado
|
||||
output_thread = threading.Thread(target=read_output, daemon=True)
|
||||
output_thread.start()
|
||||
|
||||
# Monitorear finalización del proceso
|
||||
def monitor_completion():
|
||||
try:
|
||||
return_code = process.wait()
|
||||
end_time = time.time()
|
||||
execution_time = end_time - start_time
|
||||
|
||||
# Actualizar historial
|
||||
self._update_history_status(execution_id, return_code, execution_time)
|
||||
|
||||
# Remover de procesos activos
|
||||
with self.process_lock:
|
||||
if process.pid in self.running_processes:
|
||||
del self.running_processes[process.pid]
|
||||
|
||||
if return_code == 0:
|
||||
broadcast_func(f"✅ Script completado exitosamente (código: {return_code})")
|
||||
else:
|
||||
broadcast_func(f"❌ Script terminó con errores (código: {return_code})")
|
||||
|
||||
broadcast_func(f"⏱️ Tiempo de ejecución: {execution_time:.2f} segundos")
|
||||
|
||||
except Exception as e:
|
||||
broadcast_func(f"Error monitoreando proceso: {e}")
|
||||
|
||||
# Iniciar monitoreo en hilo separado
|
||||
monitor_thread = threading.Thread(target=monitor_completion, daemon=True)
|
||||
monitor_thread.start()
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Script '{script_name}' ejecutándose...",
|
||||
"execution_id": execution_id,
|
||||
"pid": process.pid,
|
||||
"background": False
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Error ejecutando script Python: {str(e)}"
|
||||
broadcast_func(error_msg)
|
||||
return {"error": error_msg}
|
||||
|
||||
def _get_python_executable(self, env_name: str) -> str:
|
||||
"""Obtener ejecutable de Python para el entorno especificado"""
|
||||
if env_name == "base":
|
||||
return sys.executable
|
||||
|
||||
# Intentar encontrar entorno de conda en todas las ubicaciones posibles
|
||||
miniconda_paths = [
|
||||
r"C:\Users\migue\miniconda3",
|
||||
r"C:\ProgramData\miniconda3",
|
||||
r"C:\miniconda3",
|
||||
os.path.expanduser("~/miniconda3"),
|
||||
os.path.expanduser("~/anaconda3")
|
||||
]
|
||||
|
||||
for base_path in miniconda_paths:
|
||||
if os.path.exists(base_path):
|
||||
env_path = os.path.join(base_path, "envs", env_name)
|
||||
python_exe = os.path.join(env_path, "python.exe")
|
||||
if os.path.exists(python_exe):
|
||||
return python_exe
|
||||
|
||||
# Fallback al Python del sistema
|
||||
print(f"Warning: Python environment '{env_name}' not found, using system Python")
|
||||
return sys.executable
|
||||
|
||||
def _load_script_metadata(self) -> Dict[str, Any]:
|
||||
"""Cargar metadatos de scripts desde archivo"""
|
||||
try:
|
||||
with open(self.script_metadata_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
return {"version": "1.0", "script_metadata": {}}
|
||||
|
||||
def _save_script_metadata(self, metadata: Dict[str, Any]):
|
||||
"""Guardar metadatos de scripts en archivo"""
|
||||
try:
|
||||
with open(self.script_metadata_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(metadata, f, indent=2, ensure_ascii=False)
|
||||
except Exception as e:
|
||||
print(f"Error saving script metadata: {e}")
|
||||
|
||||
def _cleanup_script_metadata_for_project(self, project_id: str):
|
||||
"""Limpiar metadatos de scripts al eliminar un proyecto"""
|
||||
try:
|
||||
script_metadata = self._load_script_metadata()
|
||||
if "script_metadata" in script_metadata:
|
||||
# Filtrar metadatos que no pertenezcan al proyecto eliminado
|
||||
script_metadata["script_metadata"] = {
|
||||
k: v for k, v in script_metadata["script_metadata"].items()
|
||||
if not k.startswith(f"{project_id}:")
|
||||
}
|
||||
self._save_script_metadata(script_metadata)
|
||||
except Exception as e:
|
||||
print(f"Error cleaning script metadata for project {project_id}: {e}")
|
||||
|
||||
def get_favorites(self) -> List[Dict[str, Any]]:
|
||||
"""Obtener scripts favoritos"""
|
||||
try:
|
||||
with open(self.favorites_path, 'r', encoding='utf-8') as f:
|
||||
favorites_data = json.load(f)
|
||||
return favorites_data.get("favorites", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def toggle_favorite(self, project_id: str, script_name: str) -> Dict[str, str]:
|
||||
"""Agregar o quitar de favoritos"""
|
||||
try:
|
||||
favorites_data = {"favorites": self.get_favorites()}
|
||||
|
||||
# Buscar si ya está en favoritos
|
||||
favorite_key = f"{project_id}:{script_name}"
|
||||
existing_favorite = None
|
||||
for i, fav in enumerate(favorites_data["favorites"]):
|
||||
if fav.get("project_id") == project_id and fav.get("script_name") == script_name:
|
||||
existing_favorite = i
|
||||
break
|
||||
|
||||
if existing_favorite is not None:
|
||||
# Quitar de favoritos
|
||||
del favorites_data["favorites"][existing_favorite]
|
||||
message = "Removido de favoritos"
|
||||
is_favorite = False
|
||||
else:
|
||||
# Agregar a favoritos
|
||||
project = self.get_python_project(project_id)
|
||||
if project:
|
||||
script_metadata = self.get_script_metadata(project_id, script_name)
|
||||
favorites_data["favorites"].append({
|
||||
"project_id": project_id,
|
||||
"project_name": project["name"],
|
||||
"script_name": script_name,
|
||||
"display_name": script_metadata.get("display_name", script_name.replace('.py', '')),
|
||||
"description": script_metadata.get("description", ""),
|
||||
"added_date": datetime.now().isoformat() + "Z"
|
||||
})
|
||||
message = "Agregado a favoritos"
|
||||
is_favorite = True
|
||||
else:
|
||||
return {"status": "error", "message": "Proyecto no encontrado"}
|
||||
|
||||
with open(self.favorites_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(favorites_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
return {"status": "success", "message": message, "is_favorite": is_favorite}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error gestionando favoritos: {str(e)}"}
|
||||
|
||||
def get_history(self) -> List[Dict[str, Any]]:
|
||||
"""Obtener historial de ejecuciones"""
|
||||
try:
|
||||
with open(self.history_path, 'r', encoding='utf-8') as f:
|
||||
history_data = json.load(f)
|
||||
return history_data.get("history", [])
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def clear_history(self) -> Dict[str, str]:
|
||||
"""Limpiar historial de ejecuciones"""
|
||||
try:
|
||||
history_data = {
|
||||
"history": [],
|
||||
"settings": {
|
||||
"max_entries": 100,
|
||||
"auto_cleanup_days": 30,
|
||||
"track_execution_time": True,
|
||||
"track_arguments": True
|
||||
}
|
||||
}
|
||||
with open(self.history_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(history_data, f, indent=2, ensure_ascii=False)
|
||||
return {"status": "success", "message": "Historial limpiado exitosamente"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error limpiando historial: {str(e)}"}
|
||||
|
||||
def get_categories(self) -> Dict[str, Any]:
|
||||
"""Obtener categorías disponibles"""
|
||||
try:
|
||||
with open(self.launcher_config_path, 'r', encoding='utf-8') as f:
|
||||
config = json.load(f)
|
||||
return config.get("categories", {})
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def _add_to_history(self, entry: Dict[str, Any]):
|
||||
"""Agregar entrada al historial"""
|
||||
try:
|
||||
history_data = {"history": self.get_history()}
|
||||
|
||||
# Agregar nueva entrada al inicio
|
||||
history_data["history"].insert(0, entry)
|
||||
|
||||
# Mantener máximo de entradas
|
||||
max_entries = 100
|
||||
if len(history_data["history"]) > max_entries:
|
||||
history_data["history"] = history_data["history"][:max_entries]
|
||||
|
||||
with open(self.history_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(history_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error adding to history: {e}")
|
||||
|
||||
def _cleanup_favorites_for_project(self, project_id: str):
|
||||
"""Limpiar favoritos al eliminar un proyecto"""
|
||||
try:
|
||||
favorites_data = {"favorites": self.get_favorites()}
|
||||
# Filtrar favoritos que no pertenezcan al proyecto eliminado
|
||||
favorites_data["favorites"] = [
|
||||
fav for fav in favorites_data["favorites"]
|
||||
if fav.get("project_id") != project_id
|
||||
]
|
||||
|
||||
with open(self.favorites_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(favorites_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error cleaning favorites for project {project_id}: {e}")
|
||||
|
||||
def _update_history_status(self, execution_id: str, final_code: int, final_execution_time: float):
|
||||
"""Actualizar estado final en el historial"""
|
||||
try:
|
||||
history_data = {"history": self.get_history()}
|
||||
|
||||
for entry in history_data["history"]:
|
||||
if entry.get("id") == execution_id:
|
||||
entry["status"] = "completed" if final_code == 0 else "error"
|
||||
entry["return_code"] = final_code
|
||||
entry["execution_time"] = final_execution_time
|
||||
break
|
||||
|
||||
with open(self.history_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(history_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error updating history status: {e}")
|
||||
|
||||
def focus_process(self, pid: int) -> Dict[str, str]:
|
||||
"""Intentar dar foco a un proceso por su PID (Windows)"""
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
|
||||
def enum_windows_proc(hwnd, pid):
|
||||
if ctypes.windll.user32.IsWindowVisible(hwnd):
|
||||
process_id = wintypes.DWORD()
|
||||
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(process_id))
|
||||
if process_id.value == pid:
|
||||
ctypes.windll.user32.SetForegroundWindow(hwnd)
|
||||
return False # Detener enumeración
|
||||
return True # Continuar enumeración
|
||||
|
||||
# Definir el tipo de callback
|
||||
EnumWindowsProc = ctypes.WINFUNCTYPE(ctypes.c_bool, wintypes.HWND, wintypes.LPARAM)
|
||||
callback = EnumWindowsProc(enum_windows_proc)
|
||||
|
||||
ctypes.windll.user32.EnumWindows(callback, pid)
|
||||
return {"status": "success", "message": f"Intentando dar foco al proceso {pid}"}
|
||||
else:
|
||||
return {"status": "info", "message": "Función de foco no disponible en esta plataforma"}
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error dando foco al proceso: {str(e)}"}
|
||||
|
||||
def terminate_process(self, pid: int) -> Dict[str, str]:
|
||||
"""Terminar un proceso por su PID"""
|
||||
try:
|
||||
with self.process_lock:
|
||||
if pid in self.running_processes:
|
||||
process_info = self.running_processes[pid]
|
||||
del self.running_processes[pid]
|
||||
|
||||
# Intentar terminar el proceso
|
||||
if sys.platform == "win32":
|
||||
subprocess.run(["taskkill", "/F", "/PID", str(pid)],
|
||||
capture_output=True, check=False)
|
||||
else:
|
||||
import signal
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass # Proceso ya terminado
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": f"Proceso {pid} terminado ({process_info.get('script_name', 'N/A')})"
|
||||
}
|
||||
else:
|
||||
return {"status": "error", "message": "Proceso no encontrado en la lista de procesos activos"}
|
||||
|
||||
except Exception as e:
|
||||
return {"status": "error", "message": f"Error terminando proceso: {str(e)}"}
|
||||
|
||||
def get_running_processes(self) -> List[Dict[str, Any]]:
|
||||
"""Obtener lista de procesos en ejecución"""
|
||||
try:
|
||||
with self.process_lock:
|
||||
processes = []
|
||||
dead_pids = []
|
||||
|
||||
for pid, info in self.running_processes.items():
|
||||
# Verificar si el proceso sigue vivo
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
result = subprocess.run(
|
||||
["tasklist", "/FI", f"PID eq {pid}"],
|
||||
capture_output=True, text=True, check=False
|
||||
)
|
||||
if str(pid) not in result.stdout:
|
||||
dead_pids.append(pid)
|
||||
continue
|
||||
else:
|
||||
os.kill(pid, 0) # No mata el proceso, solo verifica si existe
|
||||
except (ProcessLookupError, subprocess.SubprocessError):
|
||||
dead_pids.append(pid)
|
||||
continue
|
||||
|
||||
# Agregar información del proceso
|
||||
project = self.get_python_project(info["project_id"])
|
||||
processes.append({
|
||||
"pid": pid,
|
||||
"project_id": info["project_id"],
|
||||
"project_name": project["name"] if project else "Proyecto no encontrado",
|
||||
"script_name": info["script_name"],
|
||||
"start_time": info["start_time"],
|
||||
"execution_id": info["execution_id"],
|
||||
"working_directory": info["working_directory"],
|
||||
"is_background": info.get("is_background", False)
|
||||
})
|
||||
|
||||
# Limpiar procesos muertos
|
||||
for pid in dead_pids:
|
||||
del self.running_processes[pid]
|
||||
|
||||
return processes
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting running processes: {e}")
|
||||
return []
|
||||
|
||||
def get_markdown_files(self, project_id: str) -> List[Dict[str, Any]]:
|
||||
"""Obtener archivos Markdown de un proyecto"""
|
||||
try:
|
||||
project = self.get_python_project(project_id)
|
||||
if not project:
|
||||
return []
|
||||
|
||||
project_dir = project["directory"]
|
||||
if not os.path.isdir(project_dir):
|
||||
return []
|
||||
|
||||
markdown_files = []
|
||||
|
||||
# Buscar archivos .md en el directorio del proyecto
|
||||
for root, dirs, files in os.walk(project_dir):
|
||||
# Excluir directorios comunes que no contienen documentación relevante
|
||||
dirs[:] = [d for d in dirs if d not in ['.git', '__pycache__', '.vscode', 'node_modules']]
|
||||
|
||||
for filename in files:
|
||||
if filename.lower().endswith('.md'):
|
||||
file_path = os.path.join(root, filename)
|
||||
relative_path = os.path.relpath(file_path, project_dir)
|
||||
|
||||
# Obtener información básica del archivo
|
||||
try:
|
||||
stat = os.stat(file_path)
|
||||
size = stat.st_size
|
||||
modified = datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z"
|
||||
|
||||
# Intentar leer las primeras líneas para obtener el título
|
||||
title = filename.replace('.md', '')
|
||||
try:
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
first_line = f.readline().strip()
|
||||
if first_line.startswith('#'):
|
||||
title = first_line.lstrip('#').strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
markdown_files.append({
|
||||
"filename": filename,
|
||||
"relative_path": relative_path.replace('\\', '/'), # Normalizar separadores
|
||||
"title": title,
|
||||
"size": size,
|
||||
"modified": modified
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"Error getting file info for {file_path}: {e}")
|
||||
continue
|
||||
|
||||
# Ordenar por ruta relativa
|
||||
return sorted(markdown_files, key=lambda x: x["relative_path"])
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting markdown files for project {project_id}: {e}")
|
||||
return []
|
||||
|
||||
def read_markdown_file(self, project_id: str, relative_path: str) -> Dict[str, Any]:
|
||||
"""Obtener contenido de un archivo Markdown"""
|
||||
try:
|
||||
project = self.get_python_project(project_id)
|
||||
if not project:
|
||||
return {"error": "Proyecto no encontrado"}
|
||||
|
||||
# Construir ruta completa y validar que esté dentro del proyecto
|
||||
project_dir = os.path.abspath(project["directory"])
|
||||
file_path = os.path.abspath(os.path.join(project_dir, relative_path))
|
||||
|
||||
# Verificar que el archivo esté dentro del directorio del proyecto (seguridad)
|
||||
if not file_path.startswith(project_dir):
|
||||
return {"error": "Acceso no autorizado al archivo"}
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
return {"error": "Archivo no encontrado"}
|
||||
|
||||
# Leer contenido del archivo
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Obtener información del archivo
|
||||
stat = os.stat(file_path)
|
||||
|
||||
return {
|
||||
"content": content,
|
||||
"filename": os.path.basename(file_path),
|
||||
"relative_path": relative_path,
|
||||
"size": stat.st_size,
|
||||
"modified": datetime.fromtimestamp(stat.st_mtime).isoformat() + "Z"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {"error": f"Error leyendo archivo: {str(e)}"}
|
|
@ -620,6 +620,7 @@ class LauncherManager {
|
|||
// === GESTIÓN DE GRUPOS (actualizada) ===
|
||||
|
||||
populateGroupForm(group) {
|
||||
document.getElementById('group-id').value = group.id;
|
||||
document.getElementById('group-name').value = group.name;
|
||||
document.getElementById('group-description').value = group.description || '';
|
||||
document.getElementById('group-category').value = group.category;
|
||||
|
@ -629,6 +630,7 @@ class LauncherManager {
|
|||
}
|
||||
|
||||
clearGroupForm() {
|
||||
document.getElementById('group-id').value = '';
|
||||
document.getElementById('group-name').value = '';
|
||||
document.getElementById('group-description').value = '';
|
||||
document.getElementById('group-category').value = 'Otros';
|
||||
|
@ -640,6 +642,7 @@ class LauncherManager {
|
|||
|
||||
async saveGroup() {
|
||||
const formData = {
|
||||
id: document.getElementById('group-id').value,
|
||||
name: document.getElementById('group-name').value,
|
||||
description: document.getElementById('group-description').value,
|
||||
category: document.getElementById('group-category').value,
|
||||
|
@ -1377,15 +1380,6 @@ function switchTab(tabName) {
|
|||
window.csharpLauncherManager.init();
|
||||
}
|
||||
}
|
||||
|
||||
// Inicializar Python launcher si es la primera vez
|
||||
if (tabName === 'python') {
|
||||
if (typeof initPythonLauncher === 'function') {
|
||||
initPythonLauncher();
|
||||
} else {
|
||||
console.error('initPythonLauncher function not found! Make sure python_launcher.js is loaded.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Funciones para modales
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -128,17 +128,6 @@
|
|||
Launcher C#
|
||||
</span>
|
||||
</button>
|
||||
<button id="python-tab" onclick="switchTab('python')"
|
||||
class="tab-button py-2 px-1 border-b-2 font-medium text-sm">
|
||||
<span class="flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M7 16h.01">
|
||||
</path>
|
||||
</svg>
|
||||
Python Scripts
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -509,155 +498,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content: Python Scripts -->
|
||||
<div id="python-content" class="tab-content hidden">
|
||||
<!-- Python Project Controls -->
|
||||
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Python Scripts - MCP Servers & Background Scripts</h2>
|
||||
<button onclick="openPythonProjectEditor()"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600"
|
||||
onmousedown="if(!window.openPythonProjectEditor) alert('Python Launcher cargando...')">
|
||||
Gestionar Proyectos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Project Selector -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-2">Seleccionar Proyecto Python</label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative flex-1">
|
||||
<select id="python-project-select" class="w-full p-3 border rounded-lg pl-12"
|
||||
onchange="loadPythonScripts()">
|
||||
<option value="">-- Seleccionar Proyecto --</option>
|
||||
</select>
|
||||
<div class="absolute left-3 top-1/2 transform -translate-y-1/2">
|
||||
<div id="selected-python-project-icon"
|
||||
class="w-6 h-6 bg-gray-200 rounded flex items-center justify-center text-sm">🐍
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="openPythonProjectInEditor('vscode')"
|
||||
class="bg-blue-500 text-white px-4 py-3 rounded-lg hover:bg-blue-600" id="vscode-python-btn"
|
||||
style="display: none;" title="Abrir proyecto en VS Code">
|
||||
<img src="{{ url_for('static', filename='icons/vscode.png') }}" class="w-5 h-5"
|
||||
alt="VS Code Icon">
|
||||
</button>
|
||||
<button onclick="openPythonProjectInEditor('cursor')"
|
||||
class="bg-purple-500 text-white px-4 py-3 rounded-lg hover:bg-purple-600"
|
||||
id="cursor-python-btn" style="display: none;" title="Abrir proyecto en Cursor">
|
||||
<img src="{{ url_for('static', filename='icons/cursor.png') }}" class="w-5 h-5"
|
||||
alt="Cursor Icon">
|
||||
</button>
|
||||
<button onclick="openPythonProjectFolder()"
|
||||
class="bg-green-500 text-white px-4 py-3 rounded-lg hover:bg-green-600"
|
||||
id="folder-python-btn" style="display: none;" title="Abrir carpeta del proyecto">
|
||||
📁
|
||||
</button>
|
||||
<button onclick="copyPythonProjectPath()"
|
||||
class="bg-gray-500 text-white px-4 py-3 rounded-lg hover:bg-gray-600"
|
||||
id="copy-path-python-btn" style="display: none;" title="Copiar path del proyecto">
|
||||
📋
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Category Filter -->
|
||||
<div class="mb-4">
|
||||
<h3 class="text-sm font-medium mb-2">Filtrar por Categoría</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="python-category-btn active px-3 py-1 rounded-full text-sm border"
|
||||
data-category="all" onclick="filterPythonByCategory('all')">
|
||||
Todas
|
||||
</button>
|
||||
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
|
||||
data-category="MCP Servers" onclick="filterPythonByCategory('MCP Servers')">
|
||||
🔌 MCP Servers
|
||||
</button>
|
||||
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
|
||||
data-category="Flask Apps" onclick="filterPythonByCategory('Flask Apps')">
|
||||
🌐 Flask Apps
|
||||
</button>
|
||||
<button class="python-category-btn px-3 py-1 rounded-full text-sm border"
|
||||
data-category="Scripts" onclick="filterPythonByCategory('Scripts')">
|
||||
📜 Scripts
|
||||
</button>
|
||||
<button class="python-category-btn px-3 py-1 rounded-full text-sm border" data-category="Bots"
|
||||
onclick="filterPythonByCategory('Bots')">
|
||||
🤖 Bots
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Favorites Panel -->
|
||||
<div id="python-favorites-panel" class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="text-lg font-semibold text-green-800">
|
||||
⭐ Scripts Favoritos
|
||||
</h3>
|
||||
<span class="text-sm text-green-600" id="python-favorites-count">
|
||||
0 favoritos
|
||||
</span>
|
||||
</div>
|
||||
<div id="python-favorites-list" class="space-y-2">
|
||||
<!-- Lista dinámica de favoritos Python -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Scripts Grid -->
|
||||
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">Scripts Disponibles</h2>
|
||||
<button onclick="openPythonScriptManager()"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||
id="manage-python-scripts-btn" style="display: none;">
|
||||
Gestionar Scripts
|
||||
</button>
|
||||
</div>
|
||||
<div id="python-scripts-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- Script cards dinámicos -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Markdown Files Section -->
|
||||
<div id="python-markdown-files-section" class="mb-6 bg-white p-6 rounded-lg shadow" style="display: none;">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold">📄 Documentación (Markdown)</h2>
|
||||
<span class="text-sm text-gray-500">Archivos .md en el directorio del proyecto</span>
|
||||
</div>
|
||||
<div id="python-markdown-files-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<!-- Markdown files cards dinámicos -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Running Processes Panel -->
|
||||
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">🔄 Procesos en Ejecución</h3>
|
||||
<button onclick="refreshPythonProcesses()" class="text-blue-500 hover:text-blue-700 text-sm">
|
||||
Actualizar
|
||||
</button>
|
||||
</div>
|
||||
<div id="python-running-processes" class="space-y-2">
|
||||
<!-- Lista dinámica de procesos -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History Panel -->
|
||||
<div class="mb-6 bg-white p-6 rounded-lg shadow">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold">📋 Historial de Ejecuciones</h3>
|
||||
<button onclick="clearPythonHistory()" class="text-red-500 hover:text-red-700 text-sm">
|
||||
Limpiar Historial
|
||||
</button>
|
||||
</div>
|
||||
<div id="python-history-list" class="space-y-2 max-h-64 overflow-y-auto">
|
||||
<!-- Lista dinámica de historial -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Editor de Proyectos C# -->
|
||||
<div id="csharp-project-editor-modal"
|
||||
class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
|
||||
|
@ -909,10 +749,15 @@
|
|||
|
||||
<!-- Formulario de edición -->
|
||||
<form id="group-form" class="space-y-4">
|
||||
<!-- El ID se genera automáticamente basado en el nombre -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Nombre</label>
|
||||
<input type="text" id="group-name" class="w-full p-2 border rounded" required>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">ID del Grupo</label>
|
||||
<input type="text" id="group-id" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Nombre</label>
|
||||
<input type="text" id="group-name" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
@ -1218,301 +1063,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Project Editor Modal -->
|
||||
<div id="python-project-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-screen overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-semibold mb-6">Gestionar Proyectos Python</h3>
|
||||
|
||||
<!-- Lista de proyectos existentes -->
|
||||
<div class="mb-6">
|
||||
<h4 class="font-medium mb-3">Proyectos Existentes</h4>
|
||||
<div id="existing-python-projects-list"
|
||||
class="space-y-2 max-h-40 overflow-y-auto border rounded p-2">
|
||||
<!-- Lista dinámica -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formulario de edición -->
|
||||
<form id="python-project-form" class="space-y-4"
|
||||
onsubmit="event.preventDefault(); savePythonProject();">
|
||||
<!-- El ID se genera automáticamente -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Nombre</label>
|
||||
<input type="text" id="python-project-name" class="w-full p-2 border rounded" required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Descripción</label>
|
||||
<textarea id="python-project-description" class="w-full p-2 border rounded h-20"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Categoría</label>
|
||||
<select id="python-project-category" class="w-full p-2 border rounded">
|
||||
<option value="MCP Servers">🔗 MCP Servers</option>
|
||||
<option value="Flask Apps">🌐 Flask Apps</option>
|
||||
<option value="Scripts">📝 Scripts</option>
|
||||
<option value="Bots">🤖 Bots</option>
|
||||
<option value="Data Processing">📊 Data Processing</option>
|
||||
<option value="Otros">📁 Otros</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Versión</label>
|
||||
<input type="text" id="python-project-version" class="w-full p-2 border rounded"
|
||||
value="1.0">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Entorno Python</label>
|
||||
<select id="python-project-python-env" class="w-full p-2 border rounded">
|
||||
<option value="base">base</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">Directorio</label>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="python-project-directory" class="flex-1 p-2 border rounded"
|
||||
required>
|
||||
<button type="button" onclick="browsePythonProjectDirectory()"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded">
|
||||
Explorar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button type="button" onclick="closePythonProjectEditor()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" onclick="deletePythonProject()"
|
||||
class="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600"
|
||||
id="delete-python-project-btn" style="display: none;">
|
||||
Eliminar
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Script Manager Modal -->
|
||||
<div id="python-script-manager-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-screen overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<h3 class="text-xl font-semibold mb-6">Gestionar Scripts del Proyecto</h3>
|
||||
<p class="text-gray-600 mb-4" id="python-script-manager-project-info">Selecciona un proyecto para
|
||||
gestionar sus scripts</p>
|
||||
|
||||
<!-- Lista de scripts -->
|
||||
<div class="space-y-3" id="python-script-manager-list">
|
||||
<!-- Lista dinámica de scripts -->
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4 mt-6 border-t">
|
||||
<button type="button" onclick="closePythonScriptManager()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Script Metadata Editor Modal -->
|
||||
<div id="python-script-metadata-editor-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-lg w-full max-h-screen overflow-y-auto">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Editar Metadatos del Script Python</h3>
|
||||
|
||||
<form id="python-script-metadata-form" class="space-y-4"
|
||||
onsubmit="event.preventDefault(); savePythonScriptMetadata();">
|
||||
<input type="hidden" id="edit-python-meta-project-id">
|
||||
<input type="hidden" id="edit-python-meta-script-name">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-bold mb-1">Nombre del Archivo</label>
|
||||
<p id="edit-python-meta-filename-display"
|
||||
class="text-sm text-gray-600 bg-gray-100 p-2 rounded border"></p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="edit-python-meta-display-name" class="block text-sm font-bold mb-2">Nombre a
|
||||
Mostrar</label>
|
||||
<input type="text" id="edit-python-meta-display-name" class="w-full p-2 border rounded"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="edit-python-meta-description" class="block text-sm font-bold mb-2">Descripción
|
||||
Corta</label>
|
||||
<input type="text" id="edit-python-meta-description" class="w-full p-2 border rounded">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="edit-python-meta-long-description"
|
||||
class="block text-sm font-bold mb-2">Descripción Larga / Ayuda</label>
|
||||
<textarea id="edit-python-meta-long-description" class="w-full p-2 border rounded"
|
||||
rows="5"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">Usa Markdown. Doble Enter para párrafo nuevo, dos
|
||||
espacios + Enter para salto de línea simple.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="edit-python-meta-hidden" class="form-checkbox h-5 w-5 mr-2">
|
||||
<label for="edit-python-meta-hidden" class="text-sm font-bold">Ocultar script (no aparecerá
|
||||
en la lista de ejecución)</label>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 pt-4">
|
||||
<button type="button" onclick="closePythonScriptMetadataEditor()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
||||
Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Script Description Modal -->
|
||||
<div id="python-script-description-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[80vh] overflow-hidden">
|
||||
<div class="p-6 border-b">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" id="python-desc-modal-script-name">Descripción del Script
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600" id="python-desc-modal-script-file"></p>
|
||||
</div>
|
||||
<button onclick="closePythonScriptDescription()"
|
||||
class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto max-h-[60vh]">
|
||||
<div id="python-script-description-content" class="prose prose-sm max-w-none">
|
||||
<!-- Contenido markdown renderizado -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border-t bg-gray-50 flex justify-end">
|
||||
<button onclick="closePythonScriptDescription()"
|
||||
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Markdown Viewer Modal -->
|
||||
<div id="python-markdown-viewer-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden">
|
||||
<div class="p-6 border-b">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 class="text-lg font-semibold" id="python-markdown-viewer-title">Documento Markdown</h3>
|
||||
<p class="text-sm text-gray-600" id="python-markdown-viewer-path"></p>
|
||||
</div>
|
||||
<button onclick="closePythonMarkdownViewer()"
|
||||
class="text-gray-500 hover:text-gray-700 text-2xl">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 overflow-y-auto max-h-[75vh]">
|
||||
<div id="python-markdown-viewer-content" class="prose prose-lg max-w-none">
|
||||
<!-- Contenido markdown renderizado -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 border-t bg-gray-50 flex justify-end">
|
||||
<button onclick="closePythonMarkdownViewer()"
|
||||
class="px-4 py-2 bg-gray-500 text-white rounded hover:bg-gray-600">
|
||||
Cerrar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Python Script Options Modal -->
|
||||
<div id="python-script-options-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||
<div class="p-6">
|
||||
<h3 class="text-lg font-semibold mb-4">Opciones de Ejecución - Python</h3>
|
||||
<div id="python-script-info" class="mb-4 p-3 bg-gray-50 rounded">
|
||||
<div class="font-medium" id="python-script-display-name"></div>
|
||||
<div class="text-sm text-gray-600" id="python-script-description"></div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
Argumentos de Línea de Comandos
|
||||
</label>
|
||||
<textarea id="python-script-args-input" class="w-full p-2 border rounded h-20"
|
||||
placeholder="--port 8080 --debug"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Separar argumentos con espacios. Usar comillas para valores con espacios.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium mb-1">
|
||||
Tipo de Ejecución
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="python-execution-type" value="false" checked class="mr-2">
|
||||
<span class="text-sm">🖥️ Normal</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="radio" name="python-execution-type" value="true" class="mr-2">
|
||||
<span class="text-sm">🚀 Background</span>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 mt-1">
|
||||
Normal: Ejecuta y muestra salida. Background: Para servidores MCP, Flask apps, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3 mt-6">
|
||||
<button onclick="closePythonScriptOptions()"
|
||||
class="px-4 py-2 text-gray-600 hover:text-gray-800">
|
||||
Cancelar
|
||||
</button>
|
||||
<button onclick="executePythonScriptWithOptions()"
|
||||
class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">
|
||||
Ejecutar Script
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://unpkg.com/markdown-it@14.1.0/dist/markdown-it.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/scripts.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/launcher.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/csharp_launcher.js') }}" defer></script>
|
||||
<script src="{{ url_for('static', filename='js/python_launcher.js') }}" defer></script>
|
||||
<script>
|
||||
// Inicializar markdown-it globalmente
|
||||
window.markdownit = window.markdownit || markdownit;
|
||||
|
|
Loading…
Reference in New Issue