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.
Files changed (93) hide show
  1. hanus/__init__.py +5 -0
  2. hanus/__main__.py +10 -0
  3. hanus/action_handlers.py +76 -0
  4. hanus/action_parser.py +82 -0
  5. hanus/agent_runner.py +1445 -0
  6. hanus/analysis/__init__.py +5 -0
  7. hanus/analysis/debt.py +702 -0
  8. hanus/analysis/dependencies.py +475 -0
  9. hanus/cache/__init__.py +5 -0
  10. hanus/cache/response_cache.py +560 -0
  11. hanus/config.py +401 -0
  12. hanus/connectors/__init__.py +19 -0
  13. hanus/connectors/base.py +114 -0
  14. hanus/connectors/claude_connector.py +146 -0
  15. hanus/connectors/gemini_connector.py +141 -0
  16. hanus/connectors/glm_connector.py +160 -0
  17. hanus/connectors/ollama_connector.py +174 -0
  18. hanus/connectors/openai_connector.py +122 -0
  19. hanus/connectors/registry.py +26 -0
  20. hanus/context/__init__.py +7 -0
  21. hanus/context/manager.py +837 -0
  22. hanus/context/selective.py +626 -0
  23. hanus/error_recovery/__init__.py +5 -0
  24. hanus/error_recovery/auto_fix.py +605 -0
  25. hanus/hooks/__init__.py +5 -0
  26. hanus/hooks/manager.py +247 -0
  27. hanus/instincts/__init__.py +44 -0
  28. hanus/instincts/cli.py +372 -0
  29. hanus/instincts/detector.py +281 -0
  30. hanus/instincts/evolver.py +361 -0
  31. hanus/instincts/manager.py +343 -0
  32. hanus/instincts/types.py +253 -0
  33. hanus/logger.py +81 -0
  34. hanus/memory/__init__.py +8 -0
  35. hanus/memory/manager.py +265 -0
  36. hanus/memory/types.py +119 -0
  37. hanus/monitor.py +341 -0
  38. hanus/parallel/__init__.py +5 -0
  39. hanus/parallel/executor.py +300 -0
  40. hanus/permissions.py +182 -0
  41. hanus/plan/__init__.py +8 -0
  42. hanus/plan/mode.py +267 -0
  43. hanus/plan/models.py +152 -0
  44. hanus/plugin_manager.py +754 -0
  45. hanus/plugin_registry.py +391 -0
  46. hanus/plugins/__init__.py +1 -0
  47. hanus/plugins/arena.py +630 -0
  48. hanus/plugins/code_review.py +123 -0
  49. hanus/plugins/cortex.py +1750 -0
  50. hanus/plugins/deps_check.py +27 -0
  51. hanus/plugins/git_ops.py +33 -0
  52. hanus/plugins/metasploit.py +530 -0
  53. hanus/plugins/notes.py +583 -0
  54. hanus/plugins/search_code.py +59 -0
  55. hanus/plugins/searchsploit.py +495 -0
  56. hanus/plugins/strategist.py +175 -0
  57. hanus/plugins/webui.py +5200 -0
  58. hanus/profiles.py +479 -0
  59. hanus/profiles_builtin/__init__.py +0 -0
  60. hanus/profiles_builtin/architect/profile.yaml +12 -0
  61. hanus/profiles_builtin/architect/system_prompt.txt +71 -0
  62. hanus/profiles_builtin/deep/profile.yaml +12 -0
  63. hanus/profiles_builtin/deep/system_prompt.txt +66 -0
  64. hanus/profiles_builtin/developer/__init__.py +0 -0
  65. hanus/profiles_builtin/developer/profile.yaml +9 -0
  66. hanus/profiles_builtin/developer/system_prompt.txt +176 -0
  67. hanus/profiles_builtin/speed/profile.yaml +12 -0
  68. hanus/profiles_builtin/speed/system_prompt.txt +51 -0
  69. hanus/project_tools.py +177 -0
  70. hanus/query_engine.py +1594 -0
  71. hanus/rules/__init__.py +237 -0
  72. hanus/search/__init__.py +5 -0
  73. hanus/search/semantic.py +596 -0
  74. hanus/session_manager.py +547 -0
  75. hanus/skill_manager.py +702 -0
  76. hanus/skills/__init__.py +4 -0
  77. hanus/subagent/__init__.py +8 -0
  78. hanus/subagent/agents/__init__.py +253 -0
  79. hanus/subagent/manager.py +309 -0
  80. hanus/subagent/types.py +266 -0
  81. hanus/suggestions/__init__.py +5 -0
  82. hanus/suggestions/proactive.py +451 -0
  83. hanus/tasks/__init__.py +8 -0
  84. hanus/tasks/manager.py +330 -0
  85. hanus/tasks/models.py +106 -0
  86. hanus/terminal_prompt.py +166 -0
  87. hanus/tools.py +1849 -0
  88. hanus/ui.py +939 -0
  89. hanuscode-1.0.0.dist-info/METADATA +1151 -0
  90. hanuscode-1.0.0.dist-info/RECORD +93 -0
  91. hanuscode-1.0.0.dist-info/WHEEL +5 -0
  92. hanuscode-1.0.0.dist-info/entry_points.txt +2 -0
  93. 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,5 @@
1
+ # hanus/parallel/__init__.py
2
+ """Parallel execution for HanusCode."""
3
+ from .executor import ParallelExecutor, Task, TaskResult
4
+
5
+ __all__ = ["ParallelExecutor", "Task", "TaskResult"]
@@ -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