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,434 @@
1
+ """Local runtime engine for executing workflows."""
2
+
3
+ import logging
4
+ from datetime import datetime, timezone
5
+ from typing import Any, Dict, List, Optional, Tuple
6
+
7
+ import networkx as nx
8
+
9
+ from kailash.nodes import Node
10
+ from kailash.sdk_exceptions import (
11
+ RuntimeExecutionError,
12
+ WorkflowExecutionError,
13
+ WorkflowValidationError,
14
+ )
15
+ from kailash.tracking import TaskManager, TaskStatus
16
+ from kailash.tracking.metrics_collector import MetricsCollector
17
+ from kailash.tracking.models import TaskMetrics
18
+ from kailash.workflow import Workflow
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class LocalRuntime:
24
+ """Local execution engine for workflows."""
25
+
26
+ def __init__(self, debug: bool = False):
27
+ """Initialize the local runtime.
28
+
29
+ Args:
30
+ debug: Whether to enable debug logging
31
+ """
32
+ self.debug = debug
33
+ self.logger = logger
34
+
35
+ if debug:
36
+ self.logger.setLevel(logging.DEBUG)
37
+ else:
38
+ self.logger.setLevel(logging.INFO)
39
+
40
+ def execute(
41
+ self,
42
+ workflow: Workflow,
43
+ task_manager: Optional[TaskManager] = None,
44
+ parameters: Optional[Dict[str, Dict[str, Any]]] = None,
45
+ ) -> Tuple[Dict[str, Any], Optional[str]]:
46
+ """Execute a workflow locally.
47
+
48
+ Args:
49
+ workflow: Workflow to execute
50
+ task_manager: Optional task manager for tracking
51
+ parameters: Optional parameter overrides per node
52
+
53
+ Returns:
54
+ Tuple of (results dict, run_id)
55
+
56
+ Raises:
57
+ RuntimeExecutionError: If execution fails
58
+ WorkflowValidationError: If workflow is invalid
59
+ """
60
+ if not workflow:
61
+ raise RuntimeExecutionError("No workflow provided")
62
+
63
+ run_id = None
64
+
65
+ try:
66
+ # Validate workflow
67
+ workflow.validate()
68
+
69
+ # Initialize tracking
70
+ if task_manager:
71
+ try:
72
+ run_id = task_manager.create_run(
73
+ workflow_name=workflow.name,
74
+ metadata={
75
+ "parameters": parameters,
76
+ "debug": self.debug,
77
+ "runtime": "local",
78
+ },
79
+ )
80
+ except Exception as e:
81
+ self.logger.warning(f"Failed to create task run: {e}")
82
+ # Continue without tracking
83
+
84
+ # Execute workflow
85
+ results = self._execute_workflow(
86
+ workflow=workflow,
87
+ task_manager=task_manager,
88
+ run_id=run_id,
89
+ parameters=parameters or {},
90
+ )
91
+
92
+ # Mark run as completed
93
+ if task_manager and run_id:
94
+ try:
95
+ task_manager.update_run_status(run_id, "completed")
96
+ except Exception as e:
97
+ self.logger.warning(f"Failed to update run status: {e}")
98
+
99
+ return results, run_id
100
+
101
+ except WorkflowValidationError:
102
+ # Re-raise validation errors as-is
103
+ if task_manager and run_id:
104
+ try:
105
+ task_manager.update_run_status(
106
+ run_id, "failed", error="Validation failed"
107
+ )
108
+ except Exception:
109
+ pass
110
+ raise
111
+ except Exception as e:
112
+ # Mark run as failed
113
+ if task_manager and run_id:
114
+ try:
115
+ task_manager.update_run_status(run_id, "failed", error=str(e))
116
+ except Exception:
117
+ pass
118
+
119
+ # Wrap other errors in RuntimeExecutionError
120
+ raise RuntimeExecutionError(
121
+ f"Workflow execution failed: {type(e).__name__}: {e}"
122
+ ) from e
123
+
124
+ def _execute_workflow(
125
+ self,
126
+ workflow: Workflow,
127
+ task_manager: Optional[TaskManager],
128
+ run_id: Optional[str],
129
+ parameters: Dict[str, Dict[str, Any]],
130
+ ) -> Dict[str, Any]:
131
+ """Execute the workflow nodes in topological order.
132
+
133
+ Args:
134
+ workflow: Workflow to execute
135
+ task_manager: Task manager for tracking
136
+ run_id: Run ID for tracking
137
+ parameters: Parameter overrides
138
+
139
+ Returns:
140
+ Dictionary of node results
141
+
142
+ Raises:
143
+ WorkflowExecutionError: If execution fails
144
+ """
145
+ # Get execution order
146
+ try:
147
+ execution_order = list(nx.topological_sort(workflow.graph))
148
+ self.logger.info(f"Execution order: {execution_order}")
149
+ except nx.NetworkXError as e:
150
+ raise WorkflowExecutionError(
151
+ f"Failed to determine execution order: {e}"
152
+ ) from e
153
+
154
+ # Initialize results storage
155
+ results = {}
156
+ node_outputs = {}
157
+ failed_nodes = []
158
+
159
+ # Execute each node
160
+ for node_id in execution_order:
161
+ self.logger.info(f"Executing node: {node_id}")
162
+
163
+ # Get node instance
164
+ node_instance = workflow._node_instances.get(node_id)
165
+ if not node_instance:
166
+ raise WorkflowExecutionError(
167
+ f"Node instance '{node_id}' not found in workflow"
168
+ )
169
+
170
+ # Start task tracking
171
+ task = None
172
+ if task_manager and run_id:
173
+ try:
174
+ # Get node metadata if available
175
+ node_metadata = {}
176
+ if hasattr(node_instance, "config") and isinstance(
177
+ node_instance.config, dict
178
+ ):
179
+ raw_metadata = node_instance.config.get("metadata", {})
180
+ # Convert NodeMetadata object to dict if needed
181
+ if hasattr(raw_metadata, "model_dump"):
182
+ node_metadata_dict = raw_metadata.model_dump()
183
+ # Convert datetime objects to strings for JSON serialization
184
+ if "created_at" in node_metadata_dict:
185
+ node_metadata_dict["created_at"] = str(
186
+ node_metadata_dict["created_at"]
187
+ )
188
+ # Convert sets to lists for JSON serialization
189
+ if "tags" in node_metadata_dict and isinstance(
190
+ node_metadata_dict["tags"], set
191
+ ):
192
+ node_metadata_dict["tags"] = list(
193
+ node_metadata_dict["tags"]
194
+ )
195
+ node_metadata = node_metadata_dict
196
+ elif isinstance(raw_metadata, dict):
197
+ node_metadata = raw_metadata
198
+
199
+ task = task_manager.create_task(
200
+ run_id=run_id,
201
+ node_id=node_id,
202
+ node_type=node_instance.__class__.__name__,
203
+ started_at=datetime.now(timezone.utc),
204
+ metadata=node_metadata,
205
+ )
206
+ # Start the task
207
+ if task:
208
+ task_manager.update_task_status(
209
+ task.task_id, TaskStatus.RUNNING
210
+ )
211
+ except Exception as e:
212
+ self.logger.warning(
213
+ f"Failed to create task for node '{node_id}': {e}"
214
+ )
215
+
216
+ try:
217
+ # Prepare inputs
218
+ inputs = self._prepare_node_inputs(
219
+ workflow=workflow,
220
+ node_id=node_id,
221
+ node_instance=node_instance,
222
+ node_outputs=node_outputs,
223
+ parameters=parameters.get(node_id, {}),
224
+ )
225
+
226
+ if self.debug:
227
+ self.logger.debug(f"Node {node_id} inputs: {inputs}")
228
+
229
+ # Execute node with metrics collection
230
+ collector = MetricsCollector()
231
+ with collector.collect(node_id=node_id) as metrics_context:
232
+ outputs = node_instance.execute(**inputs)
233
+
234
+ # Get performance metrics
235
+ performance_metrics = metrics_context.result()
236
+
237
+ # Store outputs
238
+ node_outputs[node_id] = outputs
239
+ results[node_id] = outputs
240
+
241
+ if self.debug:
242
+ self.logger.debug(f"Node {node_id} outputs: {outputs}")
243
+
244
+ # Update task status with enhanced metrics
245
+ if task and task_manager:
246
+ # Convert performance metrics to TaskMetrics format
247
+ task_metrics_data = performance_metrics.to_task_metrics()
248
+ task_metrics = TaskMetrics(**task_metrics_data)
249
+
250
+ # Update task with metrics
251
+ task_manager.update_task_status(
252
+ task.task_id,
253
+ TaskStatus.COMPLETED,
254
+ result=outputs,
255
+ ended_at=datetime.now(timezone.utc),
256
+ metadata={"execution_time": performance_metrics.duration},
257
+ )
258
+
259
+ # Update task metrics separately
260
+ task_manager.update_task_metrics(task.task_id, task_metrics)
261
+
262
+ self.logger.info(
263
+ f"Node {node_id} completed successfully in {performance_metrics.duration:.3f}s"
264
+ )
265
+
266
+ except Exception as e:
267
+ failed_nodes.append(node_id)
268
+ self.logger.error(f"Node {node_id} failed: {e}", exc_info=self.debug)
269
+
270
+ # Update task status
271
+ if task and task_manager:
272
+ task_manager.update_task_status(
273
+ task.task_id,
274
+ TaskStatus.FAILED,
275
+ error=str(e),
276
+ ended_at=datetime.now(timezone.utc),
277
+ )
278
+
279
+ # Determine if we should continue
280
+ if self._should_stop_on_error(workflow, node_id):
281
+ error_msg = f"Node '{node_id}' failed: {e}"
282
+ if len(failed_nodes) > 1:
283
+ error_msg += f" (Previously failed nodes: {failed_nodes[:-1]})"
284
+
285
+ raise WorkflowExecutionError(error_msg) from e
286
+ else:
287
+ # Continue execution but record error
288
+ results[node_id] = {
289
+ "error": str(e),
290
+ "error_type": type(e).__name__,
291
+ "failed": True,
292
+ }
293
+
294
+ return results
295
+
296
+ def _prepare_node_inputs(
297
+ self,
298
+ workflow: Workflow,
299
+ node_id: str,
300
+ node_instance: Node,
301
+ node_outputs: Dict[str, Dict[str, Any]],
302
+ parameters: Dict[str, Any],
303
+ ) -> Dict[str, Any]:
304
+ """Prepare inputs for a node execution.
305
+
306
+ Args:
307
+ workflow: The workflow being executed
308
+ node_id: Current node ID
309
+ node_instance: Current node instance
310
+ node_outputs: Outputs from previously executed nodes
311
+ parameters: Parameter overrides
312
+
313
+ Returns:
314
+ Dictionary of inputs for the node
315
+
316
+ Raises:
317
+ WorkflowExecutionError: If input preparation fails
318
+ """
319
+ inputs = {}
320
+
321
+ # Start with node configuration
322
+ inputs.update(node_instance.config)
323
+
324
+ # Add connected inputs from other nodes
325
+ for edge in workflow.graph.in_edges(node_id, data=True):
326
+ source_node_id = edge[0]
327
+ mapping = edge[2].get("mapping", {})
328
+
329
+ if source_node_id in node_outputs:
330
+ source_outputs = node_outputs[source_node_id]
331
+
332
+ # Check if the source node failed
333
+ if isinstance(source_outputs, dict) and source_outputs.get("failed"):
334
+ raise WorkflowExecutionError(
335
+ f"Cannot use outputs from failed node '{source_node_id}'"
336
+ )
337
+
338
+ for source_key, target_key in mapping.items():
339
+ if source_key in source_outputs:
340
+ inputs[target_key] = source_outputs[source_key]
341
+ else:
342
+ self.logger.warning(
343
+ f"Source output '{source_key}' not found in node '{source_node_id}'. "
344
+ f"Available outputs: {list(source_outputs.keys())}"
345
+ )
346
+
347
+ # Apply parameter overrides
348
+ inputs.update(parameters)
349
+
350
+ return inputs
351
+
352
+ def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
353
+ """Determine if execution should stop when a node fails.
354
+
355
+ Args:
356
+ workflow: The workflow being executed
357
+ node_id: Failed node ID
358
+
359
+ Returns:
360
+ Whether to stop execution
361
+ """
362
+ # Check if any downstream nodes depend on this node
363
+ has_dependents = workflow.graph.out_degree(node_id) > 0
364
+
365
+ # For now, stop if the failed node has dependents
366
+ # Future: implement configurable error handling policies
367
+ return has_dependents
368
+
369
+ def validate_workflow(self, workflow: Workflow) -> List[str]:
370
+ """Validate a workflow before execution.
371
+
372
+ Args:
373
+ workflow: Workflow to validate
374
+
375
+ Returns:
376
+ List of validation warnings (empty if valid)
377
+
378
+ Raises:
379
+ WorkflowValidationError: If workflow is invalid
380
+ """
381
+ warnings = []
382
+
383
+ try:
384
+ workflow.validate()
385
+ except WorkflowValidationError:
386
+ # Re-raise validation errors
387
+ raise
388
+ except Exception as e:
389
+ raise WorkflowValidationError(f"Workflow validation failed: {e}") from e
390
+
391
+ # Check for disconnected nodes
392
+ for node_id in workflow.graph.nodes():
393
+ if (
394
+ workflow.graph.in_degree(node_id) == 0
395
+ and workflow.graph.out_degree(node_id) == 0
396
+ and len(workflow.graph.nodes()) > 1
397
+ ):
398
+ warnings.append(f"Node '{node_id}' is disconnected from the workflow")
399
+
400
+ # Check for missing required parameters
401
+ for node_id, node_instance in workflow._node_instances.items():
402
+ try:
403
+ params = node_instance.get_parameters()
404
+ except Exception as e:
405
+ warnings.append(f"Failed to get parameters for node '{node_id}': {e}")
406
+ continue
407
+
408
+ for param_name, param_def in params.items():
409
+ if param_def.required:
410
+ # Check if provided in config or connected
411
+ if param_name not in node_instance.config:
412
+ # Check if connected from another node
413
+ incoming_params = set()
414
+ for _, _, data in workflow.graph.in_edges(node_id, data=True):
415
+ mapping = data.get("mapping", {})
416
+ incoming_params.update(mapping.values())
417
+
418
+ if (
419
+ param_name not in incoming_params
420
+ and param_def.default is None
421
+ ):
422
+ warnings.append(
423
+ f"Node '{node_id}' missing required parameter '{param_name}' "
424
+ f"(no default value provided)"
425
+ )
426
+
427
+ # Check for potential performance issues
428
+ if len(workflow.graph.nodes()) > 100:
429
+ warnings.append(
430
+ f"Large workflow with {len(workflow.graph.nodes())} nodes "
431
+ f"may have performance implications"
432
+ )
433
+
434
+ return warnings