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.
- kailash/__init__.py +31 -0
- kailash/__main__.py +11 -0
- kailash/cli/__init__.py +5 -0
- kailash/cli/commands.py +563 -0
- kailash/manifest.py +778 -0
- kailash/nodes/__init__.py +23 -0
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/agents.py +417 -0
- kailash/nodes/ai/models.py +488 -0
- kailash/nodes/api/__init__.py +52 -0
- kailash/nodes/api/auth.py +567 -0
- kailash/nodes/api/graphql.py +480 -0
- kailash/nodes/api/http.py +598 -0
- kailash/nodes/api/rate_limiting.py +572 -0
- kailash/nodes/api/rest.py +665 -0
- kailash/nodes/base.py +1032 -0
- kailash/nodes/base_async.py +128 -0
- kailash/nodes/code/__init__.py +32 -0
- kailash/nodes/code/python.py +1021 -0
- kailash/nodes/data/__init__.py +125 -0
- kailash/nodes/data/readers.py +496 -0
- kailash/nodes/data/sharepoint_graph.py +623 -0
- kailash/nodes/data/sql.py +380 -0
- kailash/nodes/data/streaming.py +1168 -0
- kailash/nodes/data/vector_db.py +964 -0
- kailash/nodes/data/writers.py +529 -0
- kailash/nodes/logic/__init__.py +6 -0
- kailash/nodes/logic/async_operations.py +702 -0
- kailash/nodes/logic/operations.py +551 -0
- kailash/nodes/transform/__init__.py +5 -0
- kailash/nodes/transform/processors.py +379 -0
- kailash/runtime/__init__.py +6 -0
- kailash/runtime/async_local.py +356 -0
- kailash/runtime/docker.py +697 -0
- kailash/runtime/local.py +434 -0
- kailash/runtime/parallel.py +557 -0
- kailash/runtime/runner.py +110 -0
- kailash/runtime/testing.py +347 -0
- kailash/sdk_exceptions.py +307 -0
- kailash/tracking/__init__.py +7 -0
- kailash/tracking/manager.py +885 -0
- kailash/tracking/metrics_collector.py +342 -0
- kailash/tracking/models.py +535 -0
- kailash/tracking/storage/__init__.py +0 -0
- kailash/tracking/storage/base.py +113 -0
- kailash/tracking/storage/database.py +619 -0
- kailash/tracking/storage/filesystem.py +543 -0
- kailash/utils/__init__.py +0 -0
- kailash/utils/export.py +924 -0
- kailash/utils/templates.py +680 -0
- kailash/visualization/__init__.py +62 -0
- kailash/visualization/api.py +732 -0
- kailash/visualization/dashboard.py +951 -0
- kailash/visualization/performance.py +808 -0
- kailash/visualization/reports.py +1471 -0
- kailash/workflow/__init__.py +15 -0
- kailash/workflow/builder.py +245 -0
- kailash/workflow/graph.py +827 -0
- kailash/workflow/mermaid_visualizer.py +628 -0
- kailash/workflow/mock_registry.py +63 -0
- kailash/workflow/runner.py +302 -0
- kailash/workflow/state.py +238 -0
- kailash/workflow/visualization.py +588 -0
- kailash-0.1.0.dist-info/METADATA +710 -0
- kailash-0.1.0.dist-info/RECORD +69 -0
- kailash-0.1.0.dist-info/WHEEL +5 -0
- kailash-0.1.0.dist-info/entry_points.txt +2 -0
- kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
- kailash-0.1.0.dist-info/top_level.txt +1 -0
kailash/runtime/local.py
ADDED
@@ -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
|