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/__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
kailash/cli/__init__.py
ADDED
kailash/cli/commands.py
ADDED
@@ -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()
|