kailash 0.1.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 (69) hide show
  1. kailash/__init__.py +31 -0
  2. kailash/__main__.py +11 -0
  3. kailash/cli/__init__.py +5 -0
  4. kailash/cli/commands.py +563 -0
  5. kailash/manifest.py +778 -0
  6. kailash/nodes/__init__.py +23 -0
  7. kailash/nodes/ai/__init__.py +26 -0
  8. kailash/nodes/ai/agents.py +417 -0
  9. kailash/nodes/ai/models.py +488 -0
  10. kailash/nodes/api/__init__.py +52 -0
  11. kailash/nodes/api/auth.py +567 -0
  12. kailash/nodes/api/graphql.py +480 -0
  13. kailash/nodes/api/http.py +598 -0
  14. kailash/nodes/api/rate_limiting.py +572 -0
  15. kailash/nodes/api/rest.py +665 -0
  16. kailash/nodes/base.py +1032 -0
  17. kailash/nodes/base_async.py +128 -0
  18. kailash/nodes/code/__init__.py +32 -0
  19. kailash/nodes/code/python.py +1021 -0
  20. kailash/nodes/data/__init__.py +125 -0
  21. kailash/nodes/data/readers.py +496 -0
  22. kailash/nodes/data/sharepoint_graph.py +623 -0
  23. kailash/nodes/data/sql.py +380 -0
  24. kailash/nodes/data/streaming.py +1168 -0
  25. kailash/nodes/data/vector_db.py +964 -0
  26. kailash/nodes/data/writers.py +529 -0
  27. kailash/nodes/logic/__init__.py +6 -0
  28. kailash/nodes/logic/async_operations.py +702 -0
  29. kailash/nodes/logic/operations.py +551 -0
  30. kailash/nodes/transform/__init__.py +5 -0
  31. kailash/nodes/transform/processors.py +379 -0
  32. kailash/runtime/__init__.py +6 -0
  33. kailash/runtime/async_local.py +356 -0
  34. kailash/runtime/docker.py +697 -0
  35. kailash/runtime/local.py +434 -0
  36. kailash/runtime/parallel.py +557 -0
  37. kailash/runtime/runner.py +110 -0
  38. kailash/runtime/testing.py +347 -0
  39. kailash/sdk_exceptions.py +307 -0
  40. kailash/tracking/__init__.py +7 -0
  41. kailash/tracking/manager.py +885 -0
  42. kailash/tracking/metrics_collector.py +342 -0
  43. kailash/tracking/models.py +535 -0
  44. kailash/tracking/storage/__init__.py +0 -0
  45. kailash/tracking/storage/base.py +113 -0
  46. kailash/tracking/storage/database.py +619 -0
  47. kailash/tracking/storage/filesystem.py +543 -0
  48. kailash/utils/__init__.py +0 -0
  49. kailash/utils/export.py +924 -0
  50. kailash/utils/templates.py +680 -0
  51. kailash/visualization/__init__.py +62 -0
  52. kailash/visualization/api.py +732 -0
  53. kailash/visualization/dashboard.py +951 -0
  54. kailash/visualization/performance.py +808 -0
  55. kailash/visualization/reports.py +1471 -0
  56. kailash/workflow/__init__.py +15 -0
  57. kailash/workflow/builder.py +245 -0
  58. kailash/workflow/graph.py +827 -0
  59. kailash/workflow/mermaid_visualizer.py +628 -0
  60. kailash/workflow/mock_registry.py +63 -0
  61. kailash/workflow/runner.py +302 -0
  62. kailash/workflow/state.py +238 -0
  63. kailash/workflow/visualization.py +588 -0
  64. kailash-0.1.0.dist-info/METADATA +710 -0
  65. kailash-0.1.0.dist-info/RECORD +69 -0
  66. kailash-0.1.0.dist-info/WHEEL +5 -0
  67. kailash-0.1.0.dist-info/entry_points.txt +2 -0
  68. kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
  69. kailash-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,885 @@
1
+ """Task manager for workflow execution tracking."""
2
+
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from kailash.sdk_exceptions import StorageException, TaskException, TaskStateError
8
+
9
+ from .models import (
10
+ RunSummary,
11
+ TaskMetrics,
12
+ TaskRun,
13
+ TaskStatus,
14
+ TaskSummary,
15
+ WorkflowRun,
16
+ )
17
+ from .storage.base import StorageBackend
18
+ from .storage.filesystem import FileSystemStorage
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class TaskManager:
24
+ """Manages task tracking for workflow executions."""
25
+
26
+ def __init__(self, storage_backend: Optional[StorageBackend] = None):
27
+ """Initialize task manager.
28
+
29
+ Args:
30
+ storage_backend: Storage backend for persistence. Defaults to FileSystemStorage.
31
+
32
+ Raises:
33
+ TaskException: If initialization fails
34
+ """
35
+ try:
36
+ self.storage = storage_backend or FileSystemStorage()
37
+ self.logger = logger
38
+
39
+ # In-memory caches
40
+ self._runs: Dict[str, WorkflowRun] = {}
41
+ self._tasks: Dict[str, TaskRun] = {}
42
+ except Exception as e:
43
+ raise TaskException(f"Failed to initialize task manager: {e}") from e
44
+
45
+ def create_run(
46
+ self, workflow_name: str, metadata: Optional[Dict[str, Any]] = None
47
+ ) -> str:
48
+ """Create a new workflow run.
49
+
50
+ Args:
51
+ workflow_name: Name of the workflow
52
+ metadata: Optional metadata for the run
53
+
54
+ Returns:
55
+ Run ID
56
+
57
+ Raises:
58
+ TaskException: If run creation fails
59
+ StorageException: If storage operation fails
60
+ """
61
+ if not workflow_name:
62
+ raise TaskException("Workflow name is required")
63
+
64
+ try:
65
+ run = WorkflowRun(workflow_name=workflow_name, metadata=metadata or {})
66
+ except Exception as e:
67
+ raise TaskException(f"Failed to create workflow run: {e}") from e
68
+
69
+ # Store in memory and persist
70
+ self._runs[run.run_id] = run
71
+
72
+ try:
73
+ self.storage.save_run(run)
74
+ except Exception as e:
75
+ # Remove from cache if storage fails
76
+ self._runs.pop(run.run_id, None)
77
+ raise StorageException(f"Failed to persist workflow run: {e}") from e
78
+
79
+ self.logger.info(f"Created workflow run: {run.run_id}")
80
+ return run.run_id
81
+
82
+ def update_run_status(
83
+ self, run_id: str, status: str, error: Optional[str] = None
84
+ ) -> None:
85
+ """Update workflow run status.
86
+
87
+ Args:
88
+ run_id: Run ID
89
+ status: New status
90
+ error: Optional error message
91
+
92
+ Raises:
93
+ TaskException: If run not found
94
+ StorageException: If storage operation fails
95
+ TaskStateError: If status transition is invalid
96
+ """
97
+ if not run_id:
98
+ raise TaskException("Run ID is required")
99
+
100
+ run = self._runs.get(run_id)
101
+ if not run:
102
+ try:
103
+ run = self.storage.load_run(run_id)
104
+ except Exception as e:
105
+ raise StorageException(f"Failed to load run '{run_id}': {e}") from e
106
+
107
+ if not run:
108
+ raise TaskException(
109
+ f"Run '{run_id}' not found. Available runs: {list(self._runs.keys())}"
110
+ )
111
+ self._runs[run_id] = run
112
+
113
+ try:
114
+ run.update_status(status, error)
115
+ except ValueError as e:
116
+ raise TaskStateError(
117
+ f"Invalid status transition for run '{run_id}': {e}"
118
+ ) from e
119
+ except Exception as e:
120
+ raise TaskException(f"Failed to update run status: {e}") from e
121
+
122
+ try:
123
+ self.storage.save_run(run)
124
+ except Exception as e:
125
+ raise StorageException(f"Failed to persist run status update: {e}") from e
126
+
127
+ self.logger.info(f"Updated run {run_id} status to: {status}")
128
+
129
+ def create_task(
130
+ self,
131
+ node_id: str,
132
+ input_data: Optional[Dict[str, Any]] = None,
133
+ metadata: Optional[Dict[str, Any]] = None,
134
+ run_id: str = "test-run-id",
135
+ node_type: str = "default-node-type",
136
+ dependencies: Optional[List[str]] = None,
137
+ started_at: Optional[datetime] = None,
138
+ ) -> TaskRun:
139
+ """Create a new task.
140
+
141
+ Args:
142
+ node_id: Node ID in the workflow
143
+ input_data: Input data for the task
144
+ metadata: Additional metadata
145
+ run_id: Associated run ID (defaults to "test-run-id" for backward compatibility)
146
+ node_type: Type of node (defaults to "default-node-type" for backward compatibility)
147
+ dependencies: List of task IDs this task depends on
148
+ started_at: When the task started
149
+
150
+ Returns:
151
+ TaskRun instance
152
+
153
+ Raises:
154
+ TaskException: If task creation fails
155
+ StorageException: If storage operation fails
156
+ """
157
+ if not node_id:
158
+ raise TaskException("Node ID is required")
159
+
160
+ try:
161
+ task = TaskRun(
162
+ run_id=run_id,
163
+ node_id=node_id,
164
+ node_type=node_type,
165
+ started_at=started_at,
166
+ input_data=input_data,
167
+ metadata=metadata or {},
168
+ dependencies=dependencies or [],
169
+ )
170
+ except Exception as e:
171
+ raise TaskException(f"Failed to create task: {e}") from e
172
+
173
+ # Store in memory and persist
174
+ self._tasks[task.task_id] = task
175
+
176
+ try:
177
+ self.storage.save_task(task)
178
+ except Exception as e:
179
+ # Remove from cache if storage fails
180
+ self._tasks.pop(task.task_id, None)
181
+ raise StorageException(f"Failed to persist task: {e}") from e
182
+
183
+ # Add task to run
184
+ run = self._runs.get(run_id)
185
+ if run:
186
+ try:
187
+ run.add_task(task.task_id)
188
+ self.storage.save_run(run)
189
+ except Exception as e:
190
+ self.logger.warning(f"Failed to add task to run: {e}")
191
+ # Continue - task is created, just not linked to run
192
+
193
+ self.logger.info(f"Created task: {task.task_id} for node {node_id}")
194
+ return task
195
+
196
+ def update_task_status(
197
+ self,
198
+ task_id: str,
199
+ status: TaskStatus,
200
+ result: Optional[Dict[str, Any]] = None,
201
+ error: Optional[str] = None,
202
+ ended_at: Optional[datetime] = None,
203
+ metadata: Optional[Dict[str, Any]] = None,
204
+ ) -> None:
205
+ """Update task status.
206
+
207
+ Args:
208
+ task_id: Task ID
209
+ status: New status
210
+ result: Task result
211
+ error: Error message
212
+ ended_at: When the task ended
213
+ metadata: Additional metadata
214
+
215
+ Raises:
216
+ TaskException: If task not found
217
+ StorageException: If storage operation fails
218
+ TaskStateError: If status transition is invalid
219
+ """
220
+ if not task_id:
221
+ raise TaskException("Task ID is required")
222
+
223
+ task = self._tasks.get(task_id)
224
+ if not task:
225
+ try:
226
+ task = self.storage.load_task(task_id)
227
+ except Exception as e:
228
+ raise StorageException(f"Failed to load task '{task_id}': {e}") from e
229
+
230
+ if not task:
231
+ raise TaskException(
232
+ f"Task '{task_id}' not found. Available tasks: {list(self._tasks.keys())}"
233
+ )
234
+ self._tasks[task_id] = task
235
+
236
+ try:
237
+ task.update_status(status, result, error, ended_at, metadata)
238
+ except ValueError as e:
239
+ raise TaskStateError(
240
+ f"Invalid status transition for task '{task_id}': {e}"
241
+ ) from e
242
+ except Exception as e:
243
+ raise TaskException(f"Failed to update task status: {e}") from e
244
+
245
+ try:
246
+ self.storage.save_task(task)
247
+ except Exception as e:
248
+ raise StorageException(f"Failed to persist task status update: {e}") from e
249
+
250
+ self.logger.info(f"Updated task {task_id} status to: {status}")
251
+
252
+ def get_run(self, run_id: str) -> Optional[WorkflowRun]:
253
+ """Get workflow run by ID.
254
+
255
+ Args:
256
+ run_id: Run ID
257
+
258
+ Returns:
259
+ WorkflowRun instance or None
260
+
261
+ Raises:
262
+ StorageException: If storage operation fails
263
+ """
264
+ if not run_id:
265
+ return None
266
+
267
+ run = self._runs.get(run_id)
268
+ if not run:
269
+ try:
270
+ run = self.storage.load_run(run_id)
271
+ except Exception as e:
272
+ self.logger.error(f"Failed to load run '{run_id}': {e}")
273
+ raise StorageException(f"Failed to load run '{run_id}': {e}") from e
274
+
275
+ if run:
276
+ self._runs[run_id] = run
277
+ return run
278
+
279
+ def get_task(self, task_id: str) -> Optional[TaskRun]:
280
+ """Get task by ID.
281
+
282
+ Args:
283
+ task_id: Task ID
284
+
285
+ Returns:
286
+ TaskRun instance or None
287
+
288
+ Raises:
289
+ StorageException: If storage operation fails
290
+ """
291
+ if not task_id:
292
+ return None
293
+
294
+ task = self._tasks.get(task_id)
295
+ if not task:
296
+ try:
297
+ task = self.storage.load_task(task_id)
298
+ except Exception as e:
299
+ self.logger.error(f"Failed to load task '{task_id}': {e}")
300
+ raise StorageException(f"Failed to load task '{task_id}': {e}") from e
301
+
302
+ if task:
303
+ self._tasks[task_id] = task
304
+ return task
305
+
306
+ def list_runs(
307
+ self, workflow_name: Optional[str] = None, status: Optional[str] = None
308
+ ) -> List[RunSummary]:
309
+ """List workflow runs.
310
+
311
+ Args:
312
+ workflow_name: Filter by workflow name
313
+ status: Filter by status
314
+
315
+ Returns:
316
+ List of run summaries
317
+
318
+ Raises:
319
+ StorageException: If storage operation fails
320
+ """
321
+ try:
322
+ runs = self.storage.list_runs(workflow_name, status)
323
+ except Exception as e:
324
+ raise StorageException(f"Failed to list runs: {e}") from e
325
+
326
+ summaries = []
327
+
328
+ for run in runs:
329
+ try:
330
+ tasks = self.list_tasks(run.run_id)
331
+ task_runs = []
332
+
333
+ for task in tasks:
334
+ try:
335
+ task_run = self.get_task(task.task_id)
336
+ if task_run:
337
+ task_runs.append(task_run)
338
+ except Exception as e:
339
+ self.logger.warning(
340
+ f"Failed to load task '{task.task_id}': {e}"
341
+ )
342
+
343
+ summary = RunSummary.from_workflow_run(run, task_runs)
344
+ summaries.append(summary)
345
+
346
+ except Exception as e:
347
+ self.logger.warning(
348
+ f"Failed to create summary for run '{run.run_id}': {e}"
349
+ )
350
+
351
+ return summaries
352
+
353
+ def list_tasks(
354
+ self,
355
+ run_id: str,
356
+ node_id: Optional[str] = None,
357
+ status: Optional[TaskStatus] = None,
358
+ ) -> List[TaskSummary]:
359
+ """List tasks for a run.
360
+
361
+ Args:
362
+ run_id: Run ID
363
+ node_id: Filter by node ID
364
+ status: Filter by status
365
+
366
+ Returns:
367
+ List of task summaries
368
+
369
+ Raises:
370
+ TaskException: If run_id is not provided
371
+ StorageException: If storage operation fails
372
+ """
373
+ if not run_id:
374
+ raise TaskException("Run ID is required")
375
+
376
+ try:
377
+ tasks = self.storage.list_tasks(run_id, node_id, status)
378
+ except Exception as e:
379
+ raise StorageException(
380
+ f"Failed to list tasks for run '{run_id}': {e}"
381
+ ) from e
382
+
383
+ summaries = []
384
+ for task in tasks:
385
+ try:
386
+ summary = TaskSummary.from_task_run(task)
387
+ summaries.append(summary)
388
+ except Exception as e:
389
+ self.logger.warning(
390
+ f"Failed to create summary for task '{task.task_id}': {e}"
391
+ )
392
+
393
+ return summaries
394
+
395
+ def get_run_summary(self, run_id: str) -> Optional[RunSummary]:
396
+ """Get summary for a specific run.
397
+
398
+ Args:
399
+ run_id: Run ID
400
+
401
+ Returns:
402
+ RunSummary or None
403
+
404
+ Raises:
405
+ StorageException: If storage operation fails
406
+ """
407
+ if not run_id:
408
+ return None
409
+
410
+ try:
411
+ run = self.get_run(run_id)
412
+ except Exception as e:
413
+ self.logger.error(f"Failed to get run '{run_id}': {e}")
414
+ return None
415
+
416
+ if not run:
417
+ return None
418
+
419
+ try:
420
+ tasks = self.list_tasks(run_id)
421
+ task_runs = []
422
+
423
+ for task in tasks:
424
+ try:
425
+ task_run = self.get_task(task.task_id)
426
+ if task_run:
427
+ task_runs.append(task_run)
428
+ except Exception as e:
429
+ self.logger.warning(f"Failed to load task '{task.task_id}': {e}")
430
+
431
+ return RunSummary.from_workflow_run(run, task_runs)
432
+
433
+ except Exception as e:
434
+ self.logger.error(f"Failed to create run summary for '{run_id}': {e}")
435
+ return None
436
+
437
+ def clear_cache(self) -> None:
438
+ """Clear in-memory caches."""
439
+ self._runs.clear()
440
+ self._tasks.clear()
441
+ self.logger.info("Cleared task manager cache")
442
+
443
+ def complete_task(
444
+ self, task_id: str, output_data: Optional[Dict[str, Any]] = None
445
+ ) -> None:
446
+ """Complete a task successfully.
447
+
448
+ Args:
449
+ task_id: Task ID
450
+ output_data: Output data for the task
451
+
452
+ Raises:
453
+ TaskException: If task not found
454
+ StorageException: If storage operation fails
455
+ """
456
+ task = self.get_task(task_id)
457
+ if not task:
458
+ raise TaskException(f"Task '{task_id}' not found")
459
+
460
+ task.complete(output_data)
461
+
462
+ # Add simple metrics if not present
463
+ if not task.metrics:
464
+ task.metrics = TaskMetrics(duration=task.duration or 0)
465
+
466
+ try:
467
+ self.storage.save_task(task)
468
+ except Exception as e:
469
+ raise StorageException(f"Failed to save completed task: {e}") from e
470
+
471
+ self.logger.info(f"Completed task {task_id}")
472
+
473
+ def fail_task(self, task_id: str, error_message: str) -> None:
474
+ """Mark a task as failed.
475
+
476
+ Args:
477
+ task_id: Task ID
478
+ error_message: Error message
479
+
480
+ Raises:
481
+ TaskException: If task not found
482
+ StorageException: If storage operation fails
483
+ """
484
+ task = self.get_task(task_id)
485
+ if not task:
486
+ raise TaskException(f"Task '{task_id}' not found")
487
+
488
+ task.fail(error_message)
489
+
490
+ try:
491
+ self.storage.save_task(task)
492
+ except Exception as e:
493
+ raise StorageException(f"Failed to save failed task: {e}") from e
494
+
495
+ self.logger.info(f"Failed task {task_id}: {error_message}")
496
+
497
+ def cancel_task(self, task_id: str, reason: str) -> None:
498
+ """Cancel a task.
499
+
500
+ Args:
501
+ task_id: Task ID
502
+ reason: Cancellation reason
503
+
504
+ Raises:
505
+ TaskException: If task not found
506
+ StorageException: If storage operation fails
507
+ """
508
+ task = self.get_task(task_id)
509
+ if not task:
510
+ raise TaskException(f"Task '{task_id}' not found")
511
+
512
+ task.cancel(reason)
513
+
514
+ try:
515
+ self.storage.save_task(task)
516
+ except Exception as e:
517
+ raise StorageException(f"Failed to save cancelled task: {e}") from e
518
+
519
+ self.logger.info(f"Cancelled task {task_id}: {reason}")
520
+
521
+ def retry_task(self, task_id: str) -> TaskRun:
522
+ """Create a new task as a retry of an existing task.
523
+
524
+ Args:
525
+ task_id: Original task ID
526
+
527
+ Returns:
528
+ New task instance
529
+
530
+ Raises:
531
+ TaskException: If task not found
532
+ StorageException: If storage operation fails
533
+ """
534
+ original_task = self.get_task(task_id)
535
+ if not original_task:
536
+ raise TaskException(f"Task '{task_id}' not found")
537
+
538
+ retry_task = original_task.create_retry()
539
+
540
+ try:
541
+ self.storage.save_task(retry_task)
542
+ except Exception as e:
543
+ raise StorageException(f"Failed to save retry task: {e}") from e
544
+
545
+ self._tasks[retry_task.task_id] = retry_task
546
+ self.logger.info(f"Created retry task {retry_task.task_id} for {task_id}")
547
+
548
+ return retry_task
549
+
550
+ def delete_task(self, task_id: str) -> None:
551
+ """Delete a task.
552
+
553
+ Args:
554
+ task_id: Task ID
555
+
556
+ Raises:
557
+ TaskException: If task not found
558
+ StorageException: If storage operation fails
559
+ """
560
+ if task_id in self._tasks:
561
+ del self._tasks[task_id]
562
+
563
+ try:
564
+ self.storage.delete_task(task_id)
565
+ except Exception as e:
566
+ raise StorageException(f"Failed to delete task: {e}") from e
567
+
568
+ self.logger.info(f"Deleted task {task_id}")
569
+
570
+ def get_tasks_by_status(self, status: TaskStatus) -> List[TaskRun]:
571
+ """Get tasks by status.
572
+
573
+ Args:
574
+ status: Status to filter by
575
+
576
+ Returns:
577
+ List of matching tasks
578
+
579
+ Raises:
580
+ StorageException: If storage operation fails
581
+ """
582
+ try:
583
+ if hasattr(self.storage, "query_tasks"):
584
+ return self.storage.query_tasks(status=status)
585
+ else:
586
+ # Fallback for MockStorage
587
+ return [t for t in self.storage.get_all_tasks() if t.status == status]
588
+ except Exception as e:
589
+ raise StorageException(f"Failed to query tasks by status: {e}") from e
590
+
591
+ def get_tasks_by_node(self, node_id: str) -> List[TaskRun]:
592
+ """Get tasks by node ID.
593
+
594
+ Args:
595
+ node_id: Node ID to filter by
596
+
597
+ Returns:
598
+ List of matching tasks
599
+
600
+ Raises:
601
+ StorageException: If storage operation fails
602
+ """
603
+ try:
604
+ if hasattr(self.storage, "query_tasks"):
605
+ return self.storage.query_tasks(node_id=node_id)
606
+ else:
607
+ # Fallback for MockStorage
608
+ return [t for t in self.storage.get_all_tasks() if t.node_id == node_id]
609
+ except Exception as e:
610
+ raise StorageException(f"Failed to query tasks by node: {e}") from e
611
+
612
+ def get_task_history(self, task_id: str) -> List[TaskRun]:
613
+ """Get task history (original task and all retries).
614
+
615
+ Args:
616
+ task_id: Task ID
617
+
618
+ Returns:
619
+ List of tasks in order (original first, latest retry last)
620
+
621
+ Raises:
622
+ TaskException: If task not found
623
+ StorageException: If storage operation fails
624
+ """
625
+ task = self.get_task(task_id)
626
+ if not task:
627
+ raise TaskException(f"Task '{task_id}' not found")
628
+
629
+ # Build history by following parent_task_id chain
630
+ history = []
631
+ current = task
632
+
633
+ # First, find the original task by following parent_task_id backward
634
+ while current.parent_task_id:
635
+ parent = self.get_task(current.parent_task_id)
636
+ if not parent:
637
+ break
638
+ current = parent
639
+
640
+ # Now current is the original task, build history forward
641
+ history.append(current)
642
+ while True:
643
+ # Find tasks with this task as parent
644
+ children = []
645
+ for t in self.storage.get_all_tasks():
646
+ if t.parent_task_id == current.task_id:
647
+ children.append(t)
648
+
649
+ if not children:
650
+ break
651
+
652
+ # Find the child with the lowest retry count
653
+ next_task = min(children, key=lambda t: t.retry_count)
654
+ history.append(next_task)
655
+ current = next_task
656
+
657
+ return history
658
+
659
+ def get_tasks_by_timerange(
660
+ self, start_time: datetime, end_time: datetime
661
+ ) -> List[TaskRun]:
662
+ """Get tasks created between start_time and end_time.
663
+
664
+ Args:
665
+ start_time: Start of time range
666
+ end_time: End of time range
667
+
668
+ Returns:
669
+ List of matching tasks
670
+
671
+ Raises:
672
+ StorageException: If storage operation fails
673
+ """
674
+ try:
675
+ if hasattr(self.storage, "query_tasks"):
676
+ return self.storage.query_tasks(
677
+ started_after=start_time, completed_before=end_time
678
+ )
679
+ else:
680
+ # Fallback for MockStorage
681
+ return [
682
+ t
683
+ for t in self.storage.get_all_tasks()
684
+ if t.created_at >= start_time and t.created_at <= end_time
685
+ ]
686
+ except Exception as e:
687
+ raise StorageException(f"Failed to query tasks by timerange: {e}") from e
688
+
689
+ def get_task_statistics(self) -> Dict[str, Any]:
690
+ """Get task statistics.
691
+
692
+ Returns:
693
+ Dictionary with statistics:
694
+ - total_tasks: Total number of tasks
695
+ - by_status: Count of tasks by status
696
+ - by_node: Count of tasks by node ID
697
+
698
+ Raises:
699
+ StorageException: If storage operation fails
700
+ """
701
+ try:
702
+ tasks = self.storage.get_all_tasks()
703
+ except Exception as e:
704
+ raise StorageException(f"Failed to get tasks for statistics: {e}") from e
705
+
706
+ by_status = {}
707
+ by_node = {}
708
+
709
+ for task in tasks:
710
+ # Count by status
711
+ status = task.status
712
+ by_status[status] = by_status.get(status, 0) + 1
713
+
714
+ # Count by node
715
+ node = task.node_id
716
+ by_node[node] = by_node.get(node, 0) + 1
717
+
718
+ return {"total_tasks": len(tasks), "by_status": by_status, "by_node": by_node}
719
+
720
+ def cleanup_old_tasks(self, days: int = 30) -> int:
721
+ """Delete tasks older than specified days.
722
+
723
+ Args:
724
+ days: Age in days
725
+
726
+ Returns:
727
+ Number of tasks deleted
728
+
729
+ Raises:
730
+ StorageException: If storage operation fails
731
+ """
732
+ try:
733
+ tasks = self.storage.get_all_tasks()
734
+ except Exception as e:
735
+ raise StorageException(f"Failed to get tasks for cleanup: {e}") from e
736
+
737
+ cutoff = datetime.now() - timedelta(days=days)
738
+ deleted = 0
739
+
740
+ for task in tasks:
741
+ if task.created_at and task.created_at < cutoff:
742
+ try:
743
+ self.delete_task(task.task_id)
744
+ deleted += 1
745
+ except Exception as e:
746
+ self.logger.warning(
747
+ f"Failed to delete old task {task.task_id}: {e}"
748
+ )
749
+
750
+ return deleted
751
+
752
+ def update_task_metrics(self, task_id: str, metrics: TaskMetrics) -> None:
753
+ """Update task metrics.
754
+
755
+ Args:
756
+ task_id: Task ID
757
+ metrics: Metrics to update
758
+
759
+ Raises:
760
+ TaskException: If task not found
761
+ StorageException: If storage operation fails
762
+ """
763
+ task = self.get_task(task_id)
764
+ if not task:
765
+ raise TaskException(f"Task '{task_id}' not found")
766
+
767
+ task.metrics = metrics
768
+
769
+ try:
770
+ self.storage.save_task(task)
771
+ except Exception as e:
772
+ raise StorageException(f"Failed to update task metrics: {e}") from e
773
+
774
+ self.logger.info(f"Updated metrics for task {task_id}")
775
+
776
+ def get_running_tasks(self) -> List[TaskRun]:
777
+ """Get all currently running tasks.
778
+
779
+ Returns:
780
+ List of running tasks
781
+
782
+ Raises:
783
+ StorageException: If storage operation fails
784
+ """
785
+ return self.get_tasks_by_status(TaskStatus.RUNNING)
786
+
787
+ def get_task_dependencies(self, task_id: str) -> List[TaskRun]:
788
+ """Get tasks that are dependencies for the given task.
789
+
790
+ Args:
791
+ task_id: Task ID
792
+
793
+ Returns:
794
+ List of dependency tasks
795
+
796
+ Raises:
797
+ TaskException: If task not found
798
+ StorageException: If storage operation fails
799
+ """
800
+ task = self.get_task(task_id)
801
+ if not task:
802
+ raise TaskException(f"Task '{task_id}' not found")
803
+
804
+ dependencies = []
805
+ for dep_id in task.dependencies:
806
+ dep = self.get_task(dep_id)
807
+ if dep:
808
+ dependencies.append(dep)
809
+
810
+ return dependencies
811
+
812
+ def save_task(self, task: TaskRun) -> None:
813
+ """Save a task to storage.
814
+
815
+ This is a convenience method that directly saves a task instance to storage.
816
+ For new tasks, prefer using create_task() instead.
817
+
818
+ Args:
819
+ task: TaskRun instance to save
820
+
821
+ Raises:
822
+ StorageException: If storage operation fails
823
+ """
824
+ try:
825
+ # Store in cache
826
+ self._tasks[task.task_id] = task
827
+
828
+ # Save to storage
829
+ self.storage.save_task(task)
830
+ self.logger.info(f"Saved task: {task.task_id}")
831
+
832
+ # Add task to run if needed
833
+ run = self._runs.get(task.run_id)
834
+ if run and task.task_id not in run.tasks:
835
+ run.add_task(task.task_id)
836
+ self.storage.save_run(run)
837
+
838
+ except Exception as e:
839
+ raise StorageException(f"Failed to save task: {e}") from e
840
+
841
+ def get_run_tasks(self, run_id: str) -> List[TaskRun]:
842
+ """Get all tasks for a specific run.
843
+
844
+ Args:
845
+ run_id: Run ID to get tasks for
846
+
847
+ Returns:
848
+ List of tasks in the run
849
+ """
850
+ run = self.get_run(run_id)
851
+ if not run:
852
+ return []
853
+
854
+ tasks = []
855
+ for task_id in run.tasks:
856
+ task = self.get_task(task_id)
857
+ if task:
858
+ tasks.append(task)
859
+
860
+ return tasks
861
+
862
+ def get_workflow_tasks(self, workflow_id: str) -> List[TaskRun]:
863
+ """Get all tasks for a workflow.
864
+
865
+ This is a compatibility method that returns all tasks across all runs for a workflow.
866
+ In practice, tasks are tracked per run, not per workflow.
867
+
868
+ Args:
869
+ workflow_id: Workflow ID (used to filter runs)
870
+
871
+ Returns:
872
+ List of all TaskRun objects for the workflow
873
+
874
+ Raises:
875
+ StorageException: If storage operation fails
876
+ """
877
+ try:
878
+ # Get all tasks from storage
879
+ all_tasks = self.storage.get_all_tasks()
880
+
881
+ # For now, return all tasks since we don't have a good way to filter by workflow_id
882
+ # In a real implementation, we'd need to track workflow_id in tasks or runs
883
+ return all_tasks
884
+ except Exception as e:
885
+ raise StorageException(f"Failed to get workflow tasks: {e}") from e