hanuscode 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hanus/__init__.py +5 -0
- hanus/__main__.py +10 -0
- hanus/action_handlers.py +76 -0
- hanus/action_parser.py +82 -0
- hanus/agent_runner.py +1445 -0
- hanus/analysis/__init__.py +5 -0
- hanus/analysis/debt.py +702 -0
- hanus/analysis/dependencies.py +475 -0
- hanus/cache/__init__.py +5 -0
- hanus/cache/response_cache.py +560 -0
- hanus/config.py +401 -0
- hanus/connectors/__init__.py +19 -0
- hanus/connectors/base.py +114 -0
- hanus/connectors/claude_connector.py +146 -0
- hanus/connectors/gemini_connector.py +141 -0
- hanus/connectors/glm_connector.py +160 -0
- hanus/connectors/ollama_connector.py +174 -0
- hanus/connectors/openai_connector.py +122 -0
- hanus/connectors/registry.py +26 -0
- hanus/context/__init__.py +7 -0
- hanus/context/manager.py +837 -0
- hanus/context/selective.py +626 -0
- hanus/error_recovery/__init__.py +5 -0
- hanus/error_recovery/auto_fix.py +605 -0
- hanus/hooks/__init__.py +5 -0
- hanus/hooks/manager.py +247 -0
- hanus/instincts/__init__.py +44 -0
- hanus/instincts/cli.py +372 -0
- hanus/instincts/detector.py +281 -0
- hanus/instincts/evolver.py +361 -0
- hanus/instincts/manager.py +343 -0
- hanus/instincts/types.py +253 -0
- hanus/logger.py +81 -0
- hanus/memory/__init__.py +8 -0
- hanus/memory/manager.py +265 -0
- hanus/memory/types.py +119 -0
- hanus/monitor.py +341 -0
- hanus/parallel/__init__.py +5 -0
- hanus/parallel/executor.py +300 -0
- hanus/permissions.py +182 -0
- hanus/plan/__init__.py +8 -0
- hanus/plan/mode.py +267 -0
- hanus/plan/models.py +152 -0
- hanus/plugin_manager.py +754 -0
- hanus/plugin_registry.py +391 -0
- hanus/plugins/__init__.py +1 -0
- hanus/plugins/arena.py +630 -0
- hanus/plugins/code_review.py +123 -0
- hanus/plugins/cortex.py +1750 -0
- hanus/plugins/deps_check.py +27 -0
- hanus/plugins/git_ops.py +33 -0
- hanus/plugins/metasploit.py +530 -0
- hanus/plugins/notes.py +583 -0
- hanus/plugins/search_code.py +59 -0
- hanus/plugins/searchsploit.py +495 -0
- hanus/plugins/strategist.py +175 -0
- hanus/plugins/webui.py +5200 -0
- hanus/profiles.py +479 -0
- hanus/profiles_builtin/__init__.py +0 -0
- hanus/profiles_builtin/architect/profile.yaml +12 -0
- hanus/profiles_builtin/architect/system_prompt.txt +71 -0
- hanus/profiles_builtin/deep/profile.yaml +12 -0
- hanus/profiles_builtin/deep/system_prompt.txt +66 -0
- hanus/profiles_builtin/developer/__init__.py +0 -0
- hanus/profiles_builtin/developer/profile.yaml +9 -0
- hanus/profiles_builtin/developer/system_prompt.txt +176 -0
- hanus/profiles_builtin/speed/profile.yaml +12 -0
- hanus/profiles_builtin/speed/system_prompt.txt +51 -0
- hanus/project_tools.py +177 -0
- hanus/query_engine.py +1594 -0
- hanus/rules/__init__.py +237 -0
- hanus/search/__init__.py +5 -0
- hanus/search/semantic.py +596 -0
- hanus/session_manager.py +547 -0
- hanus/skill_manager.py +702 -0
- hanus/skills/__init__.py +4 -0
- hanus/subagent/__init__.py +8 -0
- hanus/subagent/agents/__init__.py +253 -0
- hanus/subagent/manager.py +309 -0
- hanus/subagent/types.py +266 -0
- hanus/suggestions/__init__.py +5 -0
- hanus/suggestions/proactive.py +451 -0
- hanus/tasks/__init__.py +8 -0
- hanus/tasks/manager.py +330 -0
- hanus/tasks/models.py +106 -0
- hanus/terminal_prompt.py +166 -0
- hanus/tools.py +1849 -0
- hanus/ui.py +939 -0
- hanuscode-1.0.0.dist-info/METADATA +1151 -0
- hanuscode-1.0.0.dist-info/RECORD +93 -0
- hanuscode-1.0.0.dist-info/WHEEL +5 -0
- hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
- hanuscode-1.0.0.dist-info/top_level.txt +1 -0
hanus/monitor.py
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# hanus/monitor.py — Sistema de Monitoreo en Background
|
|
2
|
+
"""
|
|
3
|
+
Monitor para ejecutar comandos largos en background y recibir notificaciones.
|
|
4
|
+
|
|
5
|
+
Permite:
|
|
6
|
+
- Ejecutar comandos que tardan (builds, tests, scans)
|
|
7
|
+
- Recibir notificaciones cuando termina o hay eventos
|
|
8
|
+
- Filtrar output para solo ver lo importante
|
|
9
|
+
|
|
10
|
+
Uso:
|
|
11
|
+
from hanus.monitor import MonitorManager
|
|
12
|
+
|
|
13
|
+
monitor = MonitorManager()
|
|
14
|
+
|
|
15
|
+
# Iniciar monitor
|
|
16
|
+
monitor.start("build", "npm run build", on_event=callback)
|
|
17
|
+
|
|
18
|
+
# Ver monitores activos
|
|
19
|
+
monitor.list()
|
|
20
|
+
|
|
21
|
+
# Parar monitor
|
|
22
|
+
monitor.stop("build")
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
import subprocess
|
|
26
|
+
import threading
|
|
27
|
+
import time
|
|
28
|
+
import queue
|
|
29
|
+
import uuid
|
|
30
|
+
from dataclasses import dataclass, field
|
|
31
|
+
from typing import Dict, List, Optional, Callable, Any
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from enum import Enum
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class MonitorStatus(Enum):
|
|
37
|
+
RUNNING = "running"
|
|
38
|
+
COMPLETED = "completed"
|
|
39
|
+
FAILED = "failed"
|
|
40
|
+
STOPPED = "stopped"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class MonitorEvent:
|
|
45
|
+
"""Un evento del monitor."""
|
|
46
|
+
monitor_id: str
|
|
47
|
+
event_type: str # "line", "complete", "error"
|
|
48
|
+
content: str
|
|
49
|
+
timestamp: float = field(default_factory=time.time)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class MonitorTask:
|
|
54
|
+
"""Una tarea de monitoreo."""
|
|
55
|
+
id: str
|
|
56
|
+
name: str
|
|
57
|
+
command: str
|
|
58
|
+
status: MonitorStatus = MonitorStatus.RUNNING
|
|
59
|
+
process: Optional[subprocess.Popen] = None
|
|
60
|
+
output_file: Optional[Path] = None
|
|
61
|
+
started_at: float = field(default_factory=time.time)
|
|
62
|
+
ended_at: Optional[float] = None
|
|
63
|
+
exit_code: Optional[int] = None
|
|
64
|
+
lines_count: int = 0
|
|
65
|
+
events_matched: int = 0
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class MonitorManager:
|
|
69
|
+
"""
|
|
70
|
+
Gestiona tareas de monitoreo en background.
|
|
71
|
+
|
|
72
|
+
Cada monitor ejecuta un comando y emite eventos por cada línea
|
|
73
|
+
que coincide con el filtro configurado.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def __init__(self, output_dir: Optional[Path] = None):
|
|
77
|
+
self.monitors: Dict[str, MonitorTask] = {}
|
|
78
|
+
self._event_queues: Dict[str, queue.Queue] = {}
|
|
79
|
+
self._callbacks: Dict[str, Callable] = {}
|
|
80
|
+
self._threads: Dict[str, threading.Thread] = {}
|
|
81
|
+
self.output_dir = output_dir or (Path.home() / ".hanus" / "monitor_logs")
|
|
82
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
def start(
|
|
85
|
+
self,
|
|
86
|
+
name: str,
|
|
87
|
+
command: str,
|
|
88
|
+
filter_pattern: str = "",
|
|
89
|
+
on_event: Optional[Callable[[MonitorEvent], None]] = None,
|
|
90
|
+
cwd: Optional[Path] = None,
|
|
91
|
+
timeout: int = 3600,
|
|
92
|
+
) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Inicia un nuevo monitor.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
name: Nombre descriptivo del monitor
|
|
98
|
+
command: Comando a ejecutar
|
|
99
|
+
filter_pattern: Regex para filtrar líneas (vacío = todas)
|
|
100
|
+
on_event: Callback para cada evento
|
|
101
|
+
cwd: Directorio de trabajo
|
|
102
|
+
timeout: Timeout en segundos
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ID del monitor
|
|
106
|
+
"""
|
|
107
|
+
import re
|
|
108
|
+
|
|
109
|
+
monitor_id = f"{name}_{uuid.uuid4().hex[:8]}"
|
|
110
|
+
|
|
111
|
+
# Crear archivo de log
|
|
112
|
+
log_file = self.output_dir / f"{monitor_id}.log"
|
|
113
|
+
|
|
114
|
+
task = MonitorTask(
|
|
115
|
+
id=monitor_id,
|
|
116
|
+
name=name,
|
|
117
|
+
command=command,
|
|
118
|
+
output_file=log_file,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
self.monitors[monitor_id] = task
|
|
122
|
+
self._event_queues[monitor_id] = queue.Queue()
|
|
123
|
+
|
|
124
|
+
if on_event:
|
|
125
|
+
self._callbacks[monitor_id] = on_event
|
|
126
|
+
|
|
127
|
+
# Compilar patrón de filtro
|
|
128
|
+
pattern = re.compile(filter_pattern) if filter_pattern else None
|
|
129
|
+
|
|
130
|
+
def run_monitor():
|
|
131
|
+
try:
|
|
132
|
+
# Abrir archivo de log
|
|
133
|
+
with open(log_file, "w", encoding="utf-8") as log:
|
|
134
|
+
log.write(f"# Monitor: {name}\n")
|
|
135
|
+
log.write(f"# Command: {command}\n")
|
|
136
|
+
log.write(f"# Started: {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n")
|
|
137
|
+
log.flush()
|
|
138
|
+
|
|
139
|
+
# Ejecutar comando
|
|
140
|
+
process = subprocess.Popen(
|
|
141
|
+
command,
|
|
142
|
+
shell=True,
|
|
143
|
+
stdout=subprocess.PIPE,
|
|
144
|
+
stderr=subprocess.STDOUT,
|
|
145
|
+
text=True,
|
|
146
|
+
cwd=str(cwd) if cwd else None,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
task.process = process
|
|
150
|
+
|
|
151
|
+
# Leer output línea por línea
|
|
152
|
+
for line in process.stdout:
|
|
153
|
+
task.lines_count += 1
|
|
154
|
+
|
|
155
|
+
# Escribir a log
|
|
156
|
+
log.write(line)
|
|
157
|
+
log.flush()
|
|
158
|
+
|
|
159
|
+
# Verificar si coincide con filtro
|
|
160
|
+
should_emit = True
|
|
161
|
+
if pattern and not pattern.search(line):
|
|
162
|
+
should_emit = False
|
|
163
|
+
|
|
164
|
+
if should_emit:
|
|
165
|
+
task.events_matched += 1
|
|
166
|
+
event = MonitorEvent(
|
|
167
|
+
monitor_id=monitor_id,
|
|
168
|
+
event_type="line",
|
|
169
|
+
content=line.rstrip(),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Poner en cola
|
|
173
|
+
self._event_queues[monitor_id].put(event)
|
|
174
|
+
|
|
175
|
+
# Llamar callback
|
|
176
|
+
if monitor_id in self._callbacks:
|
|
177
|
+
try:
|
|
178
|
+
self._callbacks[monitor_id](event)
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
# Esperar a que termine
|
|
183
|
+
process.wait()
|
|
184
|
+
task.exit_code = process.returncode
|
|
185
|
+
task.ended_at = time.time()
|
|
186
|
+
task.status = MonitorStatus.COMPLETED if process.returncode == 0 else MonitorStatus.FAILED
|
|
187
|
+
|
|
188
|
+
# Emitir evento de finalización
|
|
189
|
+
final_event = MonitorEvent(
|
|
190
|
+
monitor_id=monitor_id,
|
|
191
|
+
event_type="complete" if process.returncode == 0 else "error",
|
|
192
|
+
content=f"Exit code: {process.returncode}",
|
|
193
|
+
)
|
|
194
|
+
self._event_queues[monitor_id].put(final_event)
|
|
195
|
+
|
|
196
|
+
if monitor_id in self._callbacks:
|
|
197
|
+
try:
|
|
198
|
+
self._callbacks[monitor_id](final_event)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
task.status = MonitorStatus.FAILED
|
|
204
|
+
task.ended_at = time.time()
|
|
205
|
+
|
|
206
|
+
error_event = MonitorEvent(
|
|
207
|
+
monitor_id=monitor_id,
|
|
208
|
+
event_type="error",
|
|
209
|
+
content=str(e),
|
|
210
|
+
)
|
|
211
|
+
self._event_queues[monitor_id].put(error_event)
|
|
212
|
+
|
|
213
|
+
# Iniciar thread
|
|
214
|
+
thread = threading.Thread(target=run_monitor, daemon=True)
|
|
215
|
+
thread.start()
|
|
216
|
+
self._threads[monitor_id] = thread
|
|
217
|
+
|
|
218
|
+
return monitor_id
|
|
219
|
+
|
|
220
|
+
def stop(self, monitor_id: str) -> str:
|
|
221
|
+
"""Detiene un monitor activo."""
|
|
222
|
+
task = self.monitors.get(monitor_id)
|
|
223
|
+
if not task:
|
|
224
|
+
return f"Monitor '{monitor_id}' no encontrado"
|
|
225
|
+
|
|
226
|
+
if task.status != MonitorStatus.RUNNING:
|
|
227
|
+
return f"Monitor '{monitor_id}' ya terminó"
|
|
228
|
+
|
|
229
|
+
if task.process:
|
|
230
|
+
task.process.terminate()
|
|
231
|
+
task.status = MonitorStatus.STOPPED
|
|
232
|
+
task.ended_at = time.time()
|
|
233
|
+
|
|
234
|
+
return f"Monitor '{monitor_id}' detenido"
|
|
235
|
+
|
|
236
|
+
def get(self, monitor_id: str) -> Optional[MonitorTask]:
|
|
237
|
+
"""Obtiene info de un monitor."""
|
|
238
|
+
return self.monitors.get(monitor_id)
|
|
239
|
+
|
|
240
|
+
def list(self) -> List[MonitorTask]:
|
|
241
|
+
"""Lista todos los monitores."""
|
|
242
|
+
return list(self.monitors.values())
|
|
243
|
+
|
|
244
|
+
def list_active(self) -> List[MonitorTask]:
|
|
245
|
+
"""Lista monitores activos."""
|
|
246
|
+
return [t for t in self.monitors.values() if t.status == MonitorStatus.RUNNING]
|
|
247
|
+
|
|
248
|
+
def get_events(self, monitor_id: str, block: bool = False, timeout: float = 0) -> List[MonitorEvent]:
|
|
249
|
+
"""Obtiene eventos pendientes de un monitor."""
|
|
250
|
+
if monitor_id not in self._event_queues:
|
|
251
|
+
return []
|
|
252
|
+
|
|
253
|
+
q = self._event_queues[monitor_id]
|
|
254
|
+
events = []
|
|
255
|
+
|
|
256
|
+
while True:
|
|
257
|
+
try:
|
|
258
|
+
if block:
|
|
259
|
+
event = q.get(timeout=timeout if timeout else None)
|
|
260
|
+
else:
|
|
261
|
+
event = q.get_nowait()
|
|
262
|
+
events.append(event)
|
|
263
|
+
except queue.Empty:
|
|
264
|
+
break
|
|
265
|
+
|
|
266
|
+
return events
|
|
267
|
+
|
|
268
|
+
def wait(self, monitor_id: str, timeout: float = None) -> Optional[MonitorTask]:
|
|
269
|
+
"""Espera a que un monitor termine."""
|
|
270
|
+
if monitor_id not in self._threads:
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
thread = self._threads[monitor_id]
|
|
274
|
+
thread.join(timeout=timeout)
|
|
275
|
+
|
|
276
|
+
return self.monitors.get(monitor_id)
|
|
277
|
+
|
|
278
|
+
def cleanup(self, max_age_hours: int = 24):
|
|
279
|
+
"""Limpia monitores antiguos."""
|
|
280
|
+
now = time.time()
|
|
281
|
+
to_remove = []
|
|
282
|
+
|
|
283
|
+
for mid, task in self.monitors.items():
|
|
284
|
+
if task.status != MonitorStatus.RUNNING:
|
|
285
|
+
if task.ended_at and (now - task.ended_at) > max_age_hours * 3600:
|
|
286
|
+
to_remove.append(mid)
|
|
287
|
+
|
|
288
|
+
for mid in to_remove:
|
|
289
|
+
del self.monitors[mid]
|
|
290
|
+
if mid in self._event_queues:
|
|
291
|
+
del self._event_queues[mid]
|
|
292
|
+
if mid in self._callbacks:
|
|
293
|
+
del self._callbacks[mid]
|
|
294
|
+
if mid in self._threads:
|
|
295
|
+
del self._threads[mid]
|
|
296
|
+
|
|
297
|
+
def format_status(self, task: MonitorTask) -> str:
|
|
298
|
+
"""Formatea el estado de un monitor para mostrar."""
|
|
299
|
+
status_icons = {
|
|
300
|
+
MonitorStatus.RUNNING: "🔄",
|
|
301
|
+
MonitorStatus.COMPLETED: "✅",
|
|
302
|
+
MonitorStatus.FAILED: "❌",
|
|
303
|
+
MonitorStatus.STOPPED: "⏹️",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
icon = status_icons.get(task.status, "❓")
|
|
307
|
+
|
|
308
|
+
duration = ""
|
|
309
|
+
if task.ended_at:
|
|
310
|
+
duration = f" ({task.ended_at - task.started_at:.1f}s)"
|
|
311
|
+
elif task.status == MonitorStatus.RUNNING:
|
|
312
|
+
duration = f" ({time.time() - task.started_at:.1f}s)"
|
|
313
|
+
|
|
314
|
+
lines = [
|
|
315
|
+
f"{icon} **{task.name}** `{task.id}`",
|
|
316
|
+
f" Comando: `{task.command}`",
|
|
317
|
+
f" Estado: {task.status.value}{duration}",
|
|
318
|
+
]
|
|
319
|
+
|
|
320
|
+
if task.status == MonitorStatus.RUNNING:
|
|
321
|
+
lines.append(f" Líneas: {task.lines_count} | Eventos: {task.events_matched}")
|
|
322
|
+
else:
|
|
323
|
+
lines.append(f" Total líneas: {task.lines_count}")
|
|
324
|
+
lines.append(f" Exit code: {task.exit_code}")
|
|
325
|
+
|
|
326
|
+
if task.output_file:
|
|
327
|
+
lines.append(f" Log: `{task.output_file}`")
|
|
328
|
+
|
|
329
|
+
return "\n".join(lines)
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
# Singleton global
|
|
333
|
+
_monitor_manager: Optional[MonitorManager] = None
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def get_monitor_manager() -> MonitorManager:
|
|
337
|
+
"""Obtiene el gestor de monitores."""
|
|
338
|
+
global _monitor_manager
|
|
339
|
+
if _monitor_manager is None:
|
|
340
|
+
_monitor_manager = MonitorManager()
|
|
341
|
+
return _monitor_manager
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# hanus/parallel/executor.py
|
|
2
|
+
"""
|
|
3
|
+
Ejecutor paralelo de tareas.
|
|
4
|
+
|
|
5
|
+
Detecta comandos/búsquedas independientes y los ejecuta en paralelo.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
import asyncio
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Dict, List, Optional, Any, Callable, Set, Tuple
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
14
|
+
import threading
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskStatus(Enum):
|
|
18
|
+
PENDING = "pending"
|
|
19
|
+
RUNNING = "running"
|
|
20
|
+
COMPLETED = "completed"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
CANCELLED = "cancelled"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Task:
|
|
27
|
+
"""Una tarea ejecutable."""
|
|
28
|
+
id: str
|
|
29
|
+
name: str
|
|
30
|
+
func: Callable
|
|
31
|
+
args: Tuple = ()
|
|
32
|
+
kwargs: Dict = field(default_factory=dict)
|
|
33
|
+
dependencies: List[str] = field(default_factory=list)
|
|
34
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
35
|
+
result: Any = None
|
|
36
|
+
error: Optional[str] = None
|
|
37
|
+
start_time: float = 0
|
|
38
|
+
end_time: float = 0
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> Dict:
|
|
41
|
+
return {
|
|
42
|
+
"id": self.id,
|
|
43
|
+
"name": self.name,
|
|
44
|
+
"status": self.status.value,
|
|
45
|
+
"error": self.error,
|
|
46
|
+
"duration": self.end_time - self.start_time if self.end_time else 0,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class TaskResult:
|
|
52
|
+
"""Resultado de una tarea."""
|
|
53
|
+
task_id: str
|
|
54
|
+
success: bool
|
|
55
|
+
result: Any
|
|
56
|
+
error: Optional[str] = None
|
|
57
|
+
duration: float = 0
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> Dict:
|
|
60
|
+
return {
|
|
61
|
+
"task_id": self.task_id,
|
|
62
|
+
"success": self.success,
|
|
63
|
+
"result": str(self.result)[:500] if self.result else None,
|
|
64
|
+
"error": self.error,
|
|
65
|
+
"duration": self.duration,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class DependencyGraph:
|
|
70
|
+
"""Grafo de dependencias entre tareas."""
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
self.nodes: Dict[str, Task] = {}
|
|
74
|
+
self.edges: Dict[str, Set[str]] = {} # task -> tasks it depends on
|
|
75
|
+
|
|
76
|
+
def add_task(self, task: Task) -> None:
|
|
77
|
+
self.nodes[task.id] = task
|
|
78
|
+
self.edges[task.id] = set(task.dependencies)
|
|
79
|
+
|
|
80
|
+
def get_ready_tasks(self) -> List[str]:
|
|
81
|
+
"""Obtiene tareas sin dependencias pendientes."""
|
|
82
|
+
ready = []
|
|
83
|
+
for task_id, task in self.nodes.items():
|
|
84
|
+
if task.status != TaskStatus.PENDING:
|
|
85
|
+
continue
|
|
86
|
+
# Verificar si todas las dependencias están completas
|
|
87
|
+
deps = self.edges.get(task_id, set())
|
|
88
|
+
all_done = all(
|
|
89
|
+
self.nodes[dep_id].status == TaskStatus.COMPLETED
|
|
90
|
+
for dep_id in deps if dep_id in self.nodes
|
|
91
|
+
)
|
|
92
|
+
if all_done:
|
|
93
|
+
ready.append(task_id)
|
|
94
|
+
return ready
|
|
95
|
+
|
|
96
|
+
def topological_sort(self) -> List[str]:
|
|
97
|
+
"""Ordena tareas por dependencias."""
|
|
98
|
+
visited = set()
|
|
99
|
+
result = []
|
|
100
|
+
|
|
101
|
+
def visit(task_id: str):
|
|
102
|
+
if task_id in visited:
|
|
103
|
+
return
|
|
104
|
+
visited.add(task_id)
|
|
105
|
+
for dep in self.edges.get(task_id, []):
|
|
106
|
+
if dep in self.nodes:
|
|
107
|
+
visit(dep)
|
|
108
|
+
result.append(task_id)
|
|
109
|
+
|
|
110
|
+
for task_id in self.nodes:
|
|
111
|
+
visit(task_id)
|
|
112
|
+
|
|
113
|
+
return result
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ParallelExecutor:
|
|
117
|
+
"""
|
|
118
|
+
Ejecuta tareas en paralelo respetando dependencias.
|
|
119
|
+
|
|
120
|
+
Features:
|
|
121
|
+
- Detección de tareas independientes
|
|
122
|
+
- Límite de concurrencia
|
|
123
|
+
- Manejo de errores
|
|
124
|
+
- Timeout por tarea
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(self, max_workers: int = 4):
|
|
128
|
+
self.max_workers = max_workers
|
|
129
|
+
self._task_counter = 0
|
|
130
|
+
self._lock = threading.Lock()
|
|
131
|
+
|
|
132
|
+
async def execute_parallel(
|
|
133
|
+
self,
|
|
134
|
+
tasks: List[Task],
|
|
135
|
+
timeout: float = 60.0
|
|
136
|
+
) -> List[TaskResult]:
|
|
137
|
+
"""
|
|
138
|
+
Ejecuta tareas en paralelo respetando dependencias.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
tasks: Lista de tareas a ejecutar
|
|
142
|
+
timeout: Timeout global en segundos
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Lista de resultados
|
|
146
|
+
"""
|
|
147
|
+
# Construir grafo de dependencias
|
|
148
|
+
graph = DependencyGraph()
|
|
149
|
+
for task in tasks:
|
|
150
|
+
graph.add_task(task)
|
|
151
|
+
|
|
152
|
+
results: Dict[str, TaskResult] = {}
|
|
153
|
+
running: Set[str] = set()
|
|
154
|
+
|
|
155
|
+
async def run_task(task: Task) -> TaskResult:
|
|
156
|
+
"""Ejecuta una tarea."""
|
|
157
|
+
task.status = TaskStatus.RUNNING
|
|
158
|
+
task.start_time = time.time()
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
# Ejecutar función
|
|
162
|
+
if asyncio.iscoroutinefunction(task.func):
|
|
163
|
+
result = await task.func(*task.args, **task.kwargs)
|
|
164
|
+
else:
|
|
165
|
+
# Ejecutar en thread pool
|
|
166
|
+
loop = asyncio.get_event_loop()
|
|
167
|
+
result = await loop.run_in_executor(
|
|
168
|
+
None, lambda: task.func(*task.args, **task.kwargs)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
task.result = result
|
|
172
|
+
task.status = TaskStatus.COMPLETED
|
|
173
|
+
task.end_time = time.time()
|
|
174
|
+
|
|
175
|
+
return TaskResult(
|
|
176
|
+
task_id=task.id,
|
|
177
|
+
success=True,
|
|
178
|
+
result=result,
|
|
179
|
+
duration=task.end_time - task.start_time
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
task.status = TaskStatus.FAILED
|
|
184
|
+
task.error = str(e)
|
|
185
|
+
task.end_time = time.time()
|
|
186
|
+
|
|
187
|
+
return TaskResult(
|
|
188
|
+
task_id=task.id,
|
|
189
|
+
success=False,
|
|
190
|
+
result=None,
|
|
191
|
+
error=str(e),
|
|
192
|
+
duration=task.end_time - task.start_time
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Ejecutar tareas respetando dependencias
|
|
196
|
+
pending = set(task.id for task in tasks)
|
|
197
|
+
start_time = time.time()
|
|
198
|
+
|
|
199
|
+
while pending:
|
|
200
|
+
# Verificar timeout
|
|
201
|
+
if time.time() - start_time > timeout:
|
|
202
|
+
for task_id in pending:
|
|
203
|
+
graph.nodes[task_id].status = TaskStatus.CANCELLED
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
# Obtener tareas listas
|
|
207
|
+
ready = graph.get_ready_tasks()
|
|
208
|
+
|
|
209
|
+
if not ready:
|
|
210
|
+
if running:
|
|
211
|
+
await asyncio.sleep(0.1)
|
|
212
|
+
continue
|
|
213
|
+
else:
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
# Lanzar tareas hasta el límite de concurrencia
|
|
217
|
+
for task_id in ready:
|
|
218
|
+
if len(running) >= self.max_workers:
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
task = graph.nodes[task_id]
|
|
222
|
+
task.status = TaskStatus.RUNNING
|
|
223
|
+
running.add(task_id)
|
|
224
|
+
pending.discard(task_id)
|
|
225
|
+
|
|
226
|
+
# Crear coroutine
|
|
227
|
+
asyncio.create_task(run_task(task)).add_done_callback(
|
|
228
|
+
lambda fut, tid=task_id: self._task_done(tid, results, running)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
await asyncio.sleep(0.05)
|
|
232
|
+
|
|
233
|
+
return list(results.values())
|
|
234
|
+
|
|
235
|
+
def _task_done(
|
|
236
|
+
self,
|
|
237
|
+
task_id: str,
|
|
238
|
+
results: Dict[str, TaskResult],
|
|
239
|
+
running: Set[str]
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Callback cuando una tarea termina."""
|
|
242
|
+
running.discard(task_id)
|
|
243
|
+
|
|
244
|
+
def create_task(
|
|
245
|
+
self,
|
|
246
|
+
name: str,
|
|
247
|
+
func: Callable,
|
|
248
|
+
args: Tuple = (),
|
|
249
|
+
kwargs: Dict = None,
|
|
250
|
+
dependencies: List[str] = None
|
|
251
|
+
) -> Task:
|
|
252
|
+
"""Crea una nueva tarea."""
|
|
253
|
+
with self._lock:
|
|
254
|
+
self._task_counter += 1
|
|
255
|
+
task_id = f"task_{self._task_counter}"
|
|
256
|
+
|
|
257
|
+
return Task(
|
|
258
|
+
id=task_id,
|
|
259
|
+
name=name,
|
|
260
|
+
func=func,
|
|
261
|
+
args=args,
|
|
262
|
+
kwargs=kwargs or {},
|
|
263
|
+
dependencies=dependencies or [],
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def analyze_dependencies(
|
|
267
|
+
self,
|
|
268
|
+
tasks: List[Task]
|
|
269
|
+
) -> Dict[str, List[str]]:
|
|
270
|
+
"""
|
|
271
|
+
Analiza dependencias entre tareas.
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Dict mapeando task_id -> lista de task_ids que dependen de él
|
|
275
|
+
"""
|
|
276
|
+
dependents: Dict[str, List[str]] = defaultdict(list)
|
|
277
|
+
|
|
278
|
+
for task in tasks:
|
|
279
|
+
for dep_id in task.dependencies:
|
|
280
|
+
dependents[dep_id].append(task.id)
|
|
281
|
+
|
|
282
|
+
return dict(dependents)
|
|
283
|
+
|
|
284
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
285
|
+
"""Obtiene estadísticas del ejecutor."""
|
|
286
|
+
return {
|
|
287
|
+
"max_workers": self.max_workers,
|
|
288
|
+
"tasks_created": self._task_counter,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# Instancia global
|
|
293
|
+
_executor: Optional[ParallelExecutor] = None
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def get_executor() -> ParallelExecutor:
|
|
297
|
+
global _executor
|
|
298
|
+
if _executor is None:
|
|
299
|
+
_executor = ParallelExecutor()
|
|
300
|
+
return _executor
|