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
kailash/__init__.py ADDED
@@ -0,0 +1,31 @@
1
+ """Kailash Python SDK - A framework for building workflow-based applications.
2
+
3
+ The Kailash SDK provides a comprehensive framework for creating nodes and workflows
4
+ that align with container-node architecture while allowing rapid prototyping.
5
+ """
6
+
7
+ from kailash.nodes.base import Node, NodeMetadata, NodeParameter
8
+ from kailash.runtime.local import LocalRuntime
9
+ from kailash.workflow.builder import WorkflowBuilder
10
+
11
+ # Import key components for easier access
12
+ from kailash.workflow.graph import Connection, NodeInstance, Workflow
13
+ from kailash.workflow.visualization import WorkflowVisualizer
14
+
15
+ # For backward compatibility
16
+ WorkflowGraph = Workflow
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ __all__ = [
21
+ "Workflow",
22
+ "WorkflowGraph", # Backward compatibility
23
+ "NodeInstance",
24
+ "Connection",
25
+ "WorkflowBuilder",
26
+ "WorkflowVisualizer",
27
+ "Node",
28
+ "NodeParameter",
29
+ "NodeMetadata",
30
+ "LocalRuntime",
31
+ ]
kailash/__main__.py ADDED
@@ -0,0 +1,11 @@
1
+ """
2
+ Kailash SDK CLI entry point.
3
+
4
+ This module enables running the Kailash SDK as a module with:
5
+ python -m kailash
6
+ """
7
+
8
+ from kailash.cli.commands import cli
9
+
10
+ if __name__ == "__main__":
11
+ cli()
@@ -0,0 +1,5 @@
1
+ """Kailash SDK Command Line Interface."""
2
+
3
+ from .commands import cli as main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,563 @@
1
+ """CLI commands for Kailash SDK."""
2
+
3
+ import json
4
+ import logging
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import click
10
+
11
+ from kailash.nodes import NodeRegistry
12
+ from kailash.runtime.local import LocalRuntime
13
+ from kailash.sdk_exceptions import (
14
+ CLIException,
15
+ ExportException,
16
+ KailashException,
17
+ NodeConfigurationError,
18
+ RuntimeExecutionError,
19
+ TaskException,
20
+ TemplateError,
21
+ WorkflowValidationError,
22
+ )
23
+ from kailash.tracking import TaskManager, TaskStatus
24
+ from kailash.utils.templates import TemplateManager
25
+ from kailash.workflow import Workflow
26
+
27
+ # Set up logging
28
+ logging.basicConfig(
29
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
30
+ )
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ def get_error_message(error: Exception) -> str:
35
+ """Extract a helpful error message from an exception."""
36
+ if isinstance(error, KailashException):
37
+ return str(error)
38
+ return f"{type(error).__name__}: {error}"
39
+
40
+
41
+ @click.group()
42
+ @click.version_option()
43
+ @click.option("--debug", is_flag=True, help="Enable debug mode")
44
+ def cli(debug: bool):
45
+ """Kailash SDK - Python SDK for container-node architecture."""
46
+ if debug:
47
+ logging.getLogger().setLevel(logging.DEBUG)
48
+
49
+
50
+ @cli.command()
51
+ @click.argument("name")
52
+ @click.option("--template", default="basic", help="Project template to use")
53
+ def init(name: str, template: str):
54
+ """Initialize a new Kailash project."""
55
+ if not name:
56
+ click.echo("Error: Project name is required", err=True)
57
+ sys.exit(1)
58
+
59
+ try:
60
+ template_manager = TemplateManager()
61
+ template_manager.create_project(name, template)
62
+
63
+ click.echo(f"Created new Kailash project: {name}")
64
+ click.echo(f"To get started:\n cd {name}\n kailash run example_workflow.py")
65
+
66
+ except TemplateError as e:
67
+ click.echo(f"Template error: {e}", err=True)
68
+ sys.exit(1)
69
+ except Exception as e:
70
+ click.echo(f"Error creating project: {get_error_message(e)}", err=True)
71
+ logger.error(f"Failed to create project: {e}", exc_info=True)
72
+ sys.exit(1)
73
+
74
+
75
+ @cli.command()
76
+ @click.argument("workflow_file")
77
+ @click.option("--params", "-p", help="JSON file with parameter overrides")
78
+ @click.option("--debug", is_flag=True, help="Enable debug mode")
79
+ @click.option("--no-tracking", is_flag=True, help="Disable task tracking")
80
+ def run(workflow_file: str, params: Optional[str], debug: bool, no_tracking: bool):
81
+ """Run a workflow locally."""
82
+ try:
83
+ # Validate workflow file exists
84
+ if not Path(workflow_file).exists():
85
+ raise CLIException(f"Workflow file not found: {workflow_file}")
86
+
87
+ # Load workflow
88
+ if workflow_file.endswith(".py"):
89
+ workflow = _load_python_workflow(workflow_file)
90
+ else:
91
+ raise CLIException(
92
+ "Only Python workflow files are supported. "
93
+ "File must have .py extension"
94
+ )
95
+
96
+ # Load parameter overrides
97
+ parameters = {}
98
+ if params:
99
+ try:
100
+ with open(params, "r") as f:
101
+ parameters = json.load(f)
102
+ except FileNotFoundError:
103
+ raise CLIException(f"Parameters file not found: {params}")
104
+ except json.JSONDecodeError as e:
105
+ raise CLIException(f"Invalid JSON in parameters file: {e}")
106
+
107
+ # Create runtime and task manager
108
+ runtime = LocalRuntime(debug=debug)
109
+ task_manager = None
110
+
111
+ if not no_tracking:
112
+ try:
113
+ task_manager = TaskManager()
114
+ except Exception as e:
115
+ logger.warning(f"Failed to create task manager: {e}")
116
+ click.echo("Warning: Task tracking disabled due to error", err=True)
117
+
118
+ click.echo(f"Running workflow: {workflow.name}")
119
+
120
+ # Execute workflow
121
+ results, run_id = runtime.execute(workflow, task_manager, parameters)
122
+
123
+ click.echo("Workflow completed successfully!")
124
+
125
+ if run_id:
126
+ click.echo(f"Run ID: {run_id}")
127
+ click.echo(f"To view task details: kailash tasks show {run_id}")
128
+
129
+ # Show summary of results
130
+ _display_results(results)
131
+
132
+ except RuntimeExecutionError as e:
133
+ click.echo(f"Workflow execution failed: {e}", err=True)
134
+ sys.exit(1)
135
+ except CLIException as e:
136
+ click.echo(f"Error: {e}", err=True)
137
+ sys.exit(1)
138
+ except Exception as e:
139
+ click.echo(f"Unexpected error: {get_error_message(e)}", err=True)
140
+ logger.error(f"Failed to run workflow: {e}", exc_info=True)
141
+ sys.exit(1)
142
+
143
+
144
+ @cli.command()
145
+ @click.argument("workflow_file")
146
+ def validate(workflow_file: str):
147
+ """Validate a workflow definition."""
148
+ try:
149
+ # Validate workflow file exists
150
+ if not Path(workflow_file).exists():
151
+ raise CLIException(f"Workflow file not found: {workflow_file}")
152
+
153
+ # Load workflow
154
+ if workflow_file.endswith(".py"):
155
+ workflow = _load_python_workflow(workflow_file)
156
+ else:
157
+ raise CLIException(
158
+ "Only Python workflow files are supported. "
159
+ "File must have .py extension"
160
+ )
161
+
162
+ # Validate workflow
163
+ runtime = LocalRuntime()
164
+ warnings = runtime.validate_workflow(workflow)
165
+
166
+ if warnings:
167
+ click.echo("Workflow validation warnings:")
168
+ for warning in warnings:
169
+ click.echo(f" - {warning}")
170
+ click.echo("\nWorkflow is valid with warnings")
171
+ else:
172
+ click.echo("Workflow is valid!")
173
+
174
+ except WorkflowValidationError as e:
175
+ click.echo(f"Validation failed: {e}", err=True)
176
+ sys.exit(1)
177
+ except CLIException as e:
178
+ click.echo(f"Error: {e}", err=True)
179
+ sys.exit(1)
180
+ except Exception as e:
181
+ click.echo(f"Validation failed: {get_error_message(e)}", err=True)
182
+ logger.error(f"Failed to validate workflow: {e}", exc_info=True)
183
+ sys.exit(1)
184
+
185
+
186
+ @cli.command()
187
+ @click.argument("workflow_file")
188
+ @click.argument("output_file")
189
+ @click.option(
190
+ "--format", default="yaml", type=click.Choice(["yaml", "json", "manifest"])
191
+ )
192
+ @click.option("--registry", help="Container registry URL")
193
+ def export(workflow_file: str, output_file: str, format: str, registry: Optional[str]):
194
+ """Export workflow to Kailash format."""
195
+ try:
196
+ # Validate workflow file exists
197
+ if not Path(workflow_file).exists():
198
+ raise CLIException(f"Workflow file not found: {workflow_file}")
199
+
200
+ # Load workflow
201
+ if workflow_file.endswith(".py"):
202
+ workflow = _load_python_workflow(workflow_file)
203
+ else:
204
+ raise CLIException(
205
+ "Only Python workflow files are supported. "
206
+ "File must have .py extension"
207
+ )
208
+
209
+ # Export workflow
210
+ export_config = {}
211
+ if registry:
212
+ export_config["container_registry"] = registry
213
+
214
+ workflow.export_to_kailash(
215
+ output_path=output_file, format=format, **export_config
216
+ )
217
+
218
+ click.echo(f"Exported workflow to: {output_file}")
219
+
220
+ except ExportException as e:
221
+ click.echo(f"Export failed: {e}", err=True)
222
+ sys.exit(1)
223
+ except CLIException as e:
224
+ click.echo(f"Error: {e}", err=True)
225
+ sys.exit(1)
226
+ except Exception as e:
227
+ click.echo(f"Export failed: {get_error_message(e)}", err=True)
228
+ logger.error(f"Failed to export workflow: {e}", exc_info=True)
229
+ sys.exit(1)
230
+
231
+
232
+ # Task management commands
233
+ @cli.group()
234
+ def tasks():
235
+ """Task tracking commands."""
236
+ pass
237
+
238
+
239
+ @tasks.command("list")
240
+ @click.option("--workflow", help="Filter by workflow name")
241
+ @click.option("--status", help="Filter by status")
242
+ @click.option("--limit", default=10, help="Number of runs to show")
243
+ def list_tasks(workflow: Optional[str], status: Optional[str], limit: int):
244
+ """List workflow runs."""
245
+ try:
246
+ task_manager = TaskManager()
247
+ runs = task_manager.list_runs(workflow_name=workflow, status=status)
248
+
249
+ if not runs:
250
+ click.echo("No runs found")
251
+ return
252
+
253
+ # Show recent runs
254
+ click.echo("Recent workflow runs:")
255
+ click.echo("-" * 60)
256
+
257
+ for i, run in enumerate(runs[:limit]):
258
+ status_color = {
259
+ "running": "yellow",
260
+ "completed": "green",
261
+ "failed": "red",
262
+ }.get(run.status, "white")
263
+
264
+ click.echo(
265
+ f"{run.run_id[:8]} "
266
+ f"{click.style(run.status.upper(), fg=status_color):12} "
267
+ f"{run.workflow_name:20} "
268
+ f"{run.started_at}"
269
+ )
270
+
271
+ if run.error:
272
+ click.echo(f" Error: {run.error}")
273
+
274
+ except TaskException as e:
275
+ click.echo(f"Task error: {e}", err=True)
276
+ sys.exit(1)
277
+ except Exception as e:
278
+ click.echo(f"Error listing tasks: {get_error_message(e)}", err=True)
279
+ logger.error(f"Failed to list tasks: {e}", exc_info=True)
280
+ sys.exit(1)
281
+
282
+
283
+ @tasks.command("show")
284
+ @click.argument("run_id")
285
+ @click.option("--verbose", "-v", is_flag=True, help="Show detailed task information")
286
+ def show_tasks(run_id: str, verbose: bool):
287
+ """Show details of a workflow run."""
288
+ try:
289
+ if not run_id:
290
+ raise CLIException("Run ID is required")
291
+
292
+ task_manager = TaskManager()
293
+
294
+ # Get run details
295
+ run = task_manager.get_run_summary(run_id)
296
+ if not run:
297
+ raise TaskException(
298
+ f"Run '{run_id}' not found. "
299
+ "Use 'kailash tasks list' to see available runs."
300
+ )
301
+
302
+ # Show run header
303
+ click.echo(f"Workflow: {run.workflow_name}")
304
+ click.echo(f"Run ID: {run.run_id}")
305
+ click.echo(
306
+ f"Status: {click.style(run.status.upper(), fg='green' if run.status == 'completed' else 'red')}"
307
+ )
308
+ click.echo(f"Started: {run.started_at}")
309
+
310
+ if run.ended_at:
311
+ click.echo(f"Ended: {run.ended_at}")
312
+ click.echo(f"Duration: {run.duration:.2f}s")
313
+
314
+ if run.error:
315
+ click.echo(f"Error: {run.error}")
316
+
317
+ # Show task summary
318
+ click.echo(
319
+ f"\nTasks: {run.task_count} total, "
320
+ f"{run.completed_tasks} completed, "
321
+ f"{run.failed_tasks} failed"
322
+ )
323
+
324
+ # Show individual tasks
325
+ if verbose:
326
+ tasks = task_manager.list_tasks(run_id)
327
+
328
+ if tasks:
329
+ click.echo("\nTask Details:")
330
+ click.echo("-" * 60)
331
+
332
+ for task in tasks:
333
+ status_color = {
334
+ TaskStatus.PENDING: "white",
335
+ TaskStatus.RUNNING: "yellow",
336
+ TaskStatus.COMPLETED: "green",
337
+ TaskStatus.FAILED: "red",
338
+ TaskStatus.SKIPPED: "cyan",
339
+ }.get(task.status, "white")
340
+
341
+ duration_str = f"{task.duration:.3f}s" if task.duration else "N/A"
342
+
343
+ click.echo(
344
+ f"{task.node_id:20} "
345
+ f"{click.style(task.status.value.upper(), fg=status_color):12} "
346
+ f"{duration_str:10}"
347
+ )
348
+
349
+ if task.error:
350
+ click.echo(f" Error: {task.error}")
351
+
352
+ except TaskException as e:
353
+ click.echo(f"Task error: {e}", err=True)
354
+ sys.exit(1)
355
+ except CLIException as e:
356
+ click.echo(f"Error: {e}", err=True)
357
+ sys.exit(1)
358
+ except Exception as e:
359
+ click.echo(f"Error showing tasks: {get_error_message(e)}", err=True)
360
+ logger.error(f"Failed to show tasks: {e}", exc_info=True)
361
+ sys.exit(1)
362
+
363
+
364
+ @tasks.command("clear")
365
+ @click.confirmation_option(prompt="Clear all task history?")
366
+ def clear_tasks():
367
+ """Clear all task history."""
368
+ try:
369
+ task_manager = TaskManager()
370
+ task_manager.storage.clear()
371
+ click.echo("Task history cleared")
372
+
373
+ except Exception as e:
374
+ click.echo(f"Error clearing tasks: {get_error_message(e)}", err=True)
375
+ logger.error(f"Failed to clear tasks: {e}", exc_info=True)
376
+ sys.exit(1)
377
+
378
+
379
+ # Node management commands
380
+ @cli.group()
381
+ def nodes():
382
+ """Node management commands."""
383
+ pass
384
+
385
+
386
+ @nodes.command("list")
387
+ def list_nodes():
388
+ """List available nodes."""
389
+ try:
390
+ registry = NodeRegistry()
391
+ nodes = registry.list_nodes()
392
+
393
+ if not nodes:
394
+ click.echo("No nodes registered")
395
+ return
396
+
397
+ click.echo("Available nodes:")
398
+ click.echo("-" * 40)
399
+
400
+ for name, node_class in sorted(nodes.items()):
401
+ module_name = node_class.__module__
402
+ click.echo(f"{name:20} {module_name}")
403
+
404
+ except Exception as e:
405
+ click.echo(f"Error listing nodes: {get_error_message(e)}", err=True)
406
+ logger.error(f"Failed to list nodes: {e}", exc_info=True)
407
+ sys.exit(1)
408
+
409
+
410
+ @nodes.command("info")
411
+ @click.argument("node_name")
412
+ def node_info(node_name: str):
413
+ """Show information about a node."""
414
+ try:
415
+ if not node_name:
416
+ raise CLIException("Node name is required")
417
+
418
+ registry = NodeRegistry()
419
+
420
+ try:
421
+ node_class = registry.get(node_name)
422
+ except NodeConfigurationError:
423
+ available_nodes = list(registry.list_nodes().keys())
424
+ raise CLIException(
425
+ f"Node '{node_name}' not found. "
426
+ f"Available nodes: {', '.join(available_nodes)}"
427
+ )
428
+
429
+ # Try to create instance with empty config to get metadata
430
+ # Some nodes require parameters, so provide empty dict
431
+ try:
432
+ node = node_class()
433
+ except NodeConfigurationError:
434
+ # If node requires config, create with minimal config
435
+ # This is just for getting metadata
436
+ node = None
437
+
438
+ click.echo(f"Node: {node_name}")
439
+ click.echo(f"Class: {node_class.__name__}")
440
+ click.echo(f"Module: {node_class.__module__}")
441
+
442
+ # Try to get description from docstring if node instance not available
443
+ if node and hasattr(node, "metadata") and node.metadata.description:
444
+ click.echo(f"Description: {node.metadata.description}")
445
+ elif node_class.__doc__:
446
+ # Use first line of docstring as description
447
+ description = node_class.__doc__.strip().split("\n")[0]
448
+ click.echo(f"Description: {description}")
449
+
450
+ # Show parameters - get from class method if instance not available
451
+ if node:
452
+ params = node.get_parameters()
453
+ else:
454
+ # Try to get parameters from class without instance
455
+ try:
456
+ temp_node = object.__new__(node_class)
457
+ params = temp_node.get_parameters()
458
+ except Exception:
459
+ params = {}
460
+
461
+ if params:
462
+ click.echo("\nParameters:")
463
+ for name, param in params.items():
464
+ required = "required" if param.required else "optional"
465
+ default = (
466
+ f", default={param.default}" if param.default is not None else ""
467
+ )
468
+
469
+ click.echo(f" {name}: {param.type.__name__} ({required}{default})")
470
+ if param.description:
471
+ click.echo(f" {param.description}")
472
+ else:
473
+ click.echo("\nNo parameters")
474
+
475
+ except CLIException as e:
476
+ click.echo(f"Error: {e}", err=True)
477
+ sys.exit(1)
478
+ except Exception as e:
479
+ click.echo(f"Error getting node info: {get_error_message(e)}", err=True)
480
+ logger.error(f"Failed to get node info: {e}", exc_info=True)
481
+ sys.exit(1)
482
+
483
+
484
+ # Helper functions
485
+ def _load_python_workflow(workflow_file: str) -> Workflow:
486
+ """Load a workflow from a Python file.
487
+
488
+ Args:
489
+ workflow_file: Path to Python file containing workflow
490
+
491
+ Returns:
492
+ Workflow instance
493
+
494
+ Raises:
495
+ CLIException: If workflow cannot be loaded
496
+ """
497
+ try:
498
+ # Read and execute Python file
499
+ global_scope = {}
500
+ with open(workflow_file, "r") as f:
501
+ code = f.read()
502
+
503
+ exec(code, global_scope)
504
+
505
+ # Find workflow instance
506
+ workflow = None
507
+ workflow_count = 0
508
+
509
+ for name, obj in global_scope.items():
510
+ if isinstance(obj, Workflow):
511
+ workflow = obj
512
+ workflow_count += 1
513
+
514
+ if workflow_count == 0:
515
+ raise CLIException(
516
+ "No Workflow instance found in file. "
517
+ "Make sure your file creates a Workflow object."
518
+ )
519
+ elif workflow_count > 1:
520
+ raise CLIException(
521
+ f"Multiple Workflow instances found ({workflow_count}). "
522
+ "Only one Workflow per file is supported."
523
+ )
524
+
525
+ return workflow
526
+
527
+ except FileNotFoundError:
528
+ raise CLIException(f"Workflow file not found: {workflow_file}")
529
+ except SyntaxError as e:
530
+ raise CLIException(f"Syntax error in workflow file: {e}")
531
+ except ImportError as e:
532
+ raise CLIException(f"Import error in workflow file: {e}")
533
+ except Exception as e:
534
+ if isinstance(e, CLIException):
535
+ raise
536
+ raise CLIException(f"Failed to load workflow: {get_error_message(e)}")
537
+
538
+
539
+ def _display_results(results: dict):
540
+ """Display workflow results in a readable format.
541
+
542
+ Args:
543
+ results: Dictionary of node results
544
+ """
545
+ for node_id, node_results in results.items():
546
+ click.echo(f"\n{node_id}:")
547
+
548
+ # Handle error results
549
+ if isinstance(node_results, dict) and node_results.get("failed"):
550
+ click.echo(" Status: FAILED")
551
+ click.echo(f" Error: {node_results.get('error', 'Unknown error')}")
552
+ continue
553
+
554
+ # Display normal results
555
+ for key, value in node_results.items():
556
+ if isinstance(value, (list, dict)) and len(str(value)) > 100:
557
+ click.echo(f" {key}: <{type(value).__name__} with {len(value)} items>")
558
+ else:
559
+ click.echo(f" {key}: {value}")
560
+
561
+
562
+ if __name__ == "__main__":
563
+ cli()