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
@@ -0,0 +1,588 @@
|
|
1
|
+
"""Workflow visualization utilities."""
|
2
|
+
|
3
|
+
import matplotlib
|
4
|
+
|
5
|
+
matplotlib.use("Agg") # Use non-interactive backend
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
8
|
+
|
9
|
+
import matplotlib.pyplot as plt
|
10
|
+
import networkx as nx
|
11
|
+
|
12
|
+
from kailash.tracking.manager import TaskManager
|
13
|
+
from kailash.workflow.graph import Workflow
|
14
|
+
|
15
|
+
|
16
|
+
class WorkflowVisualizer:
|
17
|
+
"""Visualize workflows as graphs."""
|
18
|
+
|
19
|
+
def __init__(
|
20
|
+
self,
|
21
|
+
workflow: Workflow,
|
22
|
+
node_colors: Optional[Dict[str, str]] = None,
|
23
|
+
edge_colors: Optional[Dict[str, str]] = None,
|
24
|
+
layout: str = "hierarchical",
|
25
|
+
):
|
26
|
+
"""Initialize visualizer.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
workflow: Workflow to visualize
|
30
|
+
node_colors: Custom node color map
|
31
|
+
edge_colors: Custom edge color map
|
32
|
+
layout: Layout algorithm to use
|
33
|
+
"""
|
34
|
+
self.workflow = workflow
|
35
|
+
self.node_colors = node_colors or self._default_node_colors()
|
36
|
+
self.edge_colors = edge_colors or self._default_edge_colors()
|
37
|
+
self.layout = layout
|
38
|
+
|
39
|
+
def _default_node_colors(self) -> Dict[str, str]:
|
40
|
+
"""Get default node color map."""
|
41
|
+
return {
|
42
|
+
"data": "lightblue",
|
43
|
+
"transform": "lightyellow",
|
44
|
+
"logic": "lightcoral",
|
45
|
+
"ai": "lightpink",
|
46
|
+
"default": "lightgray",
|
47
|
+
}
|
48
|
+
|
49
|
+
def _default_edge_colors(self) -> Dict[str, str]:
|
50
|
+
"""Get default edge color map."""
|
51
|
+
return {"default": "gray", "error": "red", "conditional": "orange"}
|
52
|
+
|
53
|
+
def _get_node_color(self, node_type: str) -> str:
|
54
|
+
"""Get color for node based on type."""
|
55
|
+
if "Reader" in node_type or "Writer" in node_type:
|
56
|
+
return self.node_colors["data"]
|
57
|
+
elif (
|
58
|
+
"Transform" in node_type
|
59
|
+
or "Filter" in node_type
|
60
|
+
or "Processor" in node_type
|
61
|
+
):
|
62
|
+
return self.node_colors["transform"]
|
63
|
+
elif "Logic" in node_type or "Merge" in node_type or "Conditional" in node_type:
|
64
|
+
return self.node_colors["logic"]
|
65
|
+
elif "AI" in node_type or "Model" in node_type:
|
66
|
+
return self.node_colors["ai"]
|
67
|
+
return self.node_colors["default"]
|
68
|
+
|
69
|
+
def _get_node_colors(self) -> List[str]:
|
70
|
+
"""Get colors for all nodes in workflow."""
|
71
|
+
colors = []
|
72
|
+
for node_id in self.workflow.graph.nodes():
|
73
|
+
node_instance = self.workflow.nodes[node_id]
|
74
|
+
node_type = node_instance.node_type
|
75
|
+
colors.append(self._get_node_color(node_type))
|
76
|
+
return colors
|
77
|
+
|
78
|
+
def _get_node_labels(self) -> Dict[str, str]:
|
79
|
+
"""Get labels for nodes in workflow."""
|
80
|
+
labels = {}
|
81
|
+
for node_id in self.workflow.graph.nodes():
|
82
|
+
# Try to get name from node instance
|
83
|
+
node = self.workflow.get_node(node_id)
|
84
|
+
if node and hasattr(node, "name") and node.name:
|
85
|
+
# For test compatibility - test expects just the name, not node_id
|
86
|
+
labels[node_id] = node.name
|
87
|
+
else:
|
88
|
+
# Fallback to node type from metadata
|
89
|
+
node_instance = self.workflow.nodes.get(node_id)
|
90
|
+
if node_instance:
|
91
|
+
labels[node_id] = f"{node_id} ({node_instance.node_type})"
|
92
|
+
else:
|
93
|
+
labels[node_id] = node_id
|
94
|
+
return labels
|
95
|
+
|
96
|
+
def _get_edge_labels(self) -> Dict[Tuple[str, str], str]:
|
97
|
+
"""Get labels for edges in workflow."""
|
98
|
+
edge_labels = {}
|
99
|
+
|
100
|
+
for edge in self.workflow.graph.edges(data=True):
|
101
|
+
source, target, data = edge
|
102
|
+
|
103
|
+
# Try both edge data formats for compatibility
|
104
|
+
from_output = data.get("from_output")
|
105
|
+
to_input = data.get("to_input")
|
106
|
+
|
107
|
+
if from_output and to_input:
|
108
|
+
edge_labels[(source, target)] = f"{from_output}→{to_input}"
|
109
|
+
else:
|
110
|
+
# Fallback to mapping format
|
111
|
+
mapping = data.get("mapping", {})
|
112
|
+
if mapping:
|
113
|
+
# Create label from mapping
|
114
|
+
label_parts = []
|
115
|
+
for src, dst in mapping.items():
|
116
|
+
label_parts.append(f"{src}→{dst}")
|
117
|
+
label = "\n".join(label_parts)
|
118
|
+
edge_labels[(source, target)] = label
|
119
|
+
|
120
|
+
return edge_labels
|
121
|
+
|
122
|
+
def _calculate_layout(self) -> Dict[str, Tuple[float, float]]:
|
123
|
+
"""Calculate node positions for visualization."""
|
124
|
+
# Try to use stored positions first
|
125
|
+
pos = {}
|
126
|
+
for node_id, node_instance in self.workflow.nodes.items():
|
127
|
+
if node_instance.position != (0, 0):
|
128
|
+
pos[node_id] = node_instance.position
|
129
|
+
|
130
|
+
# If no positions stored, calculate them
|
131
|
+
if not pos:
|
132
|
+
if self.layout == "hierarchical":
|
133
|
+
# Use hierarchical layout for DAGs
|
134
|
+
try:
|
135
|
+
# Create layers based on topological order
|
136
|
+
layers = self._create_layers()
|
137
|
+
pos = self._hierarchical_layout(layers)
|
138
|
+
except Exception:
|
139
|
+
# Fallback to spring layout
|
140
|
+
pos = nx.spring_layout(self.workflow.graph, k=3, iterations=50)
|
141
|
+
elif self.layout == "circular":
|
142
|
+
pos = nx.circular_layout(self.workflow.graph)
|
143
|
+
elif self.layout == "spring":
|
144
|
+
pos = nx.spring_layout(self.workflow.graph, k=2, iterations=100)
|
145
|
+
else:
|
146
|
+
# Default to spring layout
|
147
|
+
pos = nx.spring_layout(self.workflow.graph)
|
148
|
+
|
149
|
+
return pos
|
150
|
+
|
151
|
+
def _create_layers(self) -> Dict[int, list]:
|
152
|
+
"""Create layers of nodes for hierarchical layout."""
|
153
|
+
layers = {}
|
154
|
+
remaining = set(self.workflow.graph.nodes())
|
155
|
+
layer = 0
|
156
|
+
|
157
|
+
while remaining:
|
158
|
+
# Find nodes with no dependencies in remaining set
|
159
|
+
current_layer = []
|
160
|
+
for node in remaining:
|
161
|
+
predecessors = set(self.workflow.graph.predecessors(node))
|
162
|
+
if not predecessors.intersection(remaining):
|
163
|
+
current_layer.append(node)
|
164
|
+
|
165
|
+
if not current_layer:
|
166
|
+
# Circular dependency, break and use all remaining
|
167
|
+
current_layer = list(remaining)
|
168
|
+
|
169
|
+
layers[layer] = current_layer
|
170
|
+
remaining -= set(current_layer)
|
171
|
+
layer += 1
|
172
|
+
|
173
|
+
return layers
|
174
|
+
|
175
|
+
def _hierarchical_layout(
|
176
|
+
self, layers: Dict[int, list]
|
177
|
+
) -> Dict[str, Tuple[float, float]]:
|
178
|
+
"""Create hierarchical layout from layers."""
|
179
|
+
pos = {}
|
180
|
+
layer_height = 2.0
|
181
|
+
|
182
|
+
for layer, nodes in layers.items():
|
183
|
+
y = layer * layer_height
|
184
|
+
if len(nodes) == 1:
|
185
|
+
x_positions = [0]
|
186
|
+
else:
|
187
|
+
width = max(2.0, len(nodes) - 1)
|
188
|
+
x_positions = [
|
189
|
+
-width / 2 + i * width / (len(nodes) - 1) for i in range(len(nodes))
|
190
|
+
]
|
191
|
+
|
192
|
+
for i, node in enumerate(nodes):
|
193
|
+
pos[node] = (x_positions[i], -y) # Negative y to flow top to bottom
|
194
|
+
|
195
|
+
return pos
|
196
|
+
|
197
|
+
def _draw_graph(
|
198
|
+
self,
|
199
|
+
pos: Dict[str, Tuple[float, float]],
|
200
|
+
node_colors: List[str],
|
201
|
+
show_labels: bool,
|
202
|
+
show_connections: bool,
|
203
|
+
) -> None:
|
204
|
+
"""Draw the graph with given positions and options."""
|
205
|
+
# Draw nodes
|
206
|
+
nx.draw_networkx_nodes(
|
207
|
+
self.workflow.graph, pos, node_color=node_colors, node_size=3000, alpha=0.9
|
208
|
+
)
|
209
|
+
|
210
|
+
# Draw edges
|
211
|
+
nx.draw_networkx_edges(
|
212
|
+
self.workflow.graph,
|
213
|
+
pos,
|
214
|
+
edge_color=self.edge_colors["default"],
|
215
|
+
width=2,
|
216
|
+
alpha=0.6,
|
217
|
+
arrows=True,
|
218
|
+
arrowsize=20,
|
219
|
+
arrowstyle="->",
|
220
|
+
)
|
221
|
+
|
222
|
+
# Draw labels
|
223
|
+
if show_labels:
|
224
|
+
labels = self._get_node_labels()
|
225
|
+
nx.draw_networkx_labels(
|
226
|
+
self.workflow.graph, pos, labels, font_size=10, font_weight="bold"
|
227
|
+
)
|
228
|
+
|
229
|
+
# Draw connection labels
|
230
|
+
if show_connections:
|
231
|
+
edge_labels = self._get_edge_labels()
|
232
|
+
nx.draw_networkx_edge_labels(
|
233
|
+
self.workflow.graph, pos, edge_labels, font_size=8
|
234
|
+
)
|
235
|
+
|
236
|
+
def visualize(
|
237
|
+
self,
|
238
|
+
output_path: Optional[str] = None,
|
239
|
+
figsize: Tuple[int, int] = (12, 8),
|
240
|
+
title: Optional[str] = None,
|
241
|
+
show_labels: bool = True,
|
242
|
+
show_connections: bool = True,
|
243
|
+
dpi: int = 300,
|
244
|
+
**kwargs,
|
245
|
+
) -> None:
|
246
|
+
"""Create a visualization of the workflow.
|
247
|
+
|
248
|
+
Args:
|
249
|
+
output_path: Path to save the image (if None, shows interactive plot)
|
250
|
+
figsize: Figure size (width, height)
|
251
|
+
title: Optional title for the graph
|
252
|
+
show_labels: Whether to show node labels
|
253
|
+
show_connections: Whether to show connection labels
|
254
|
+
dpi: Resolution (dots per inch) for saved images
|
255
|
+
**kwargs: Additional options passed to plt.savefig
|
256
|
+
"""
|
257
|
+
try:
|
258
|
+
plt.figure(figsize=figsize)
|
259
|
+
|
260
|
+
# Calculate node positions
|
261
|
+
pos = self._calculate_layout()
|
262
|
+
|
263
|
+
# Handle empty workflow case
|
264
|
+
if not self.workflow.graph.nodes():
|
265
|
+
pos = {}
|
266
|
+
node_colors = []
|
267
|
+
else:
|
268
|
+
# Draw the graph with colors
|
269
|
+
node_colors = self._get_node_colors()
|
270
|
+
|
271
|
+
# Draw the graph components
|
272
|
+
if pos and node_colors:
|
273
|
+
self._draw_graph(pos, node_colors, show_labels, show_connections)
|
274
|
+
|
275
|
+
# Set title
|
276
|
+
if title is None:
|
277
|
+
title = f"Workflow: {self.workflow.name}"
|
278
|
+
plt.title(title, fontsize=16, fontweight="bold")
|
279
|
+
|
280
|
+
# Remove axes
|
281
|
+
plt.axis("off")
|
282
|
+
plt.tight_layout()
|
283
|
+
|
284
|
+
# Show or save
|
285
|
+
if output_path:
|
286
|
+
plt.savefig(output_path, dpi=dpi, bbox_inches="tight", **kwargs)
|
287
|
+
plt.close()
|
288
|
+
else:
|
289
|
+
plt.show()
|
290
|
+
except Exception as e:
|
291
|
+
plt.close()
|
292
|
+
raise e
|
293
|
+
|
294
|
+
def save(self, output_path: str, dpi: int = 300, **kwargs) -> None:
|
295
|
+
"""Save visualization to file.
|
296
|
+
|
297
|
+
Args:
|
298
|
+
output_path: Path to save the image
|
299
|
+
dpi: Resolution (dots per inch)
|
300
|
+
**kwargs: Additional options for plt.savefig
|
301
|
+
"""
|
302
|
+
kwargs["dpi"] = dpi
|
303
|
+
self.visualize(output_path=output_path, **kwargs)
|
304
|
+
|
305
|
+
def create_execution_graph(
|
306
|
+
self, run_id: str, task_manager: Any, output_path: Optional[str] = None
|
307
|
+
) -> str:
|
308
|
+
"""Create a Mermaid visualization showing execution status.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
run_id: Run ID to visualize
|
312
|
+
task_manager: Task manager instance
|
313
|
+
output_path: Optional path to save the markdown file. If not provided,
|
314
|
+
saves to 'workflow_executions/execution_{run_id}.md'
|
315
|
+
|
316
|
+
Returns:
|
317
|
+
Path to the created markdown file
|
318
|
+
"""
|
319
|
+
# Import here to avoid circular dependency
|
320
|
+
from kailash.tracking import TaskStatus
|
321
|
+
from kailash.workflow.mermaid_visualizer import MermaidVisualizer
|
322
|
+
|
323
|
+
# Get tasks for this run
|
324
|
+
tasks = task_manager.list_tasks(run_id)
|
325
|
+
|
326
|
+
# Create status emoji mapping
|
327
|
+
status_emojis = {
|
328
|
+
TaskStatus.PENDING: "⏳",
|
329
|
+
TaskStatus.RUNNING: "🔄",
|
330
|
+
TaskStatus.COMPLETED: "✅",
|
331
|
+
TaskStatus.FAILED: "❌",
|
332
|
+
TaskStatus.SKIPPED: "⏭️",
|
333
|
+
}
|
334
|
+
|
335
|
+
# Map node IDs to statuses
|
336
|
+
node_status = {}
|
337
|
+
for task in tasks:
|
338
|
+
node_status[task.node_id] = task.status
|
339
|
+
|
340
|
+
# Create Mermaid visualizer
|
341
|
+
visualizer = MermaidVisualizer(self.workflow)
|
342
|
+
|
343
|
+
# Generate base Mermaid diagram
|
344
|
+
mermaid_code = visualizer.generate()
|
345
|
+
|
346
|
+
# Add status information to the diagram
|
347
|
+
lines = mermaid_code.split("\n")
|
348
|
+
new_lines = []
|
349
|
+
|
350
|
+
for line in lines:
|
351
|
+
new_lines.append(line)
|
352
|
+
# Add status emoji to node labels
|
353
|
+
for node_id, status in node_status.items():
|
354
|
+
sanitized_id = visualizer._sanitize_node_id(node_id)
|
355
|
+
if sanitized_id in line and '["' in line:
|
356
|
+
# Add emoji to the end of the label
|
357
|
+
emoji = status_emojis.get(status, "")
|
358
|
+
if emoji:
|
359
|
+
new_lines[-1] = line.replace('"]', f' {emoji}"]')
|
360
|
+
|
361
|
+
# Create markdown content
|
362
|
+
newline_joined = "\n".join(new_lines)
|
363
|
+
markdown_content = f"""# Workflow Execution Status
|
364
|
+
|
365
|
+
**Run ID**: `{run_id}`
|
366
|
+
**Workflow**: {self.workflow.name}
|
367
|
+
**Timestamp**: {task_manager.get_run(run_id).started_at if hasattr(task_manager, 'get_run') else 'N/A'}
|
368
|
+
|
369
|
+
## Execution Diagram
|
370
|
+
|
371
|
+
```mermaid
|
372
|
+
{newline_joined}
|
373
|
+
```
|
374
|
+
|
375
|
+
## Status Legend
|
376
|
+
|
377
|
+
| Status | Symbol | Description |
|
378
|
+
|--------|--------|-------------|
|
379
|
+
| Pending | ⏳ | Task is waiting to be executed |
|
380
|
+
| Running | 🔄 | Task is currently executing |
|
381
|
+
| Completed | ✅ | Task completed successfully |
|
382
|
+
| Failed | ❌ | Task failed during execution |
|
383
|
+
| Skipped | ⏭️ | Task was skipped |
|
384
|
+
|
385
|
+
## Task Details
|
386
|
+
|
387
|
+
| Node ID | Status | Start Time | End Time | Duration |
|
388
|
+
|---------|--------|------------|----------|----------|
|
389
|
+
"""
|
390
|
+
|
391
|
+
# Add task details
|
392
|
+
for task in tasks:
|
393
|
+
status_emoji = status_emojis.get(task.status, "")
|
394
|
+
start_time = task.started_at or "N/A"
|
395
|
+
end_time = task.ended_at or "N/A"
|
396
|
+
duration = f"{task.duration:.2f}s" if task.duration else "N/A"
|
397
|
+
|
398
|
+
markdown_content += f"| {task.node_id} | {task.status.value} {status_emoji} | {start_time} | {end_time} | {duration} |\n"
|
399
|
+
|
400
|
+
# Determine output path
|
401
|
+
if output_path is None:
|
402
|
+
# Create default directory if it doesn't exist
|
403
|
+
output_dir = Path.cwd() / "outputs" / "workflow_executions"
|
404
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
405
|
+
output_path = output_dir / f"execution_{run_id}.md"
|
406
|
+
else:
|
407
|
+
output_path = Path(output_path)
|
408
|
+
# Create parent directory if needed
|
409
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
410
|
+
|
411
|
+
# Write markdown file
|
412
|
+
with open(output_path, "w") as f:
|
413
|
+
f.write(markdown_content)
|
414
|
+
|
415
|
+
return str(output_path)
|
416
|
+
|
417
|
+
def create_performance_dashboard(
|
418
|
+
self, run_id: str, task_manager: TaskManager, output_dir: Optional[Path] = None
|
419
|
+
) -> Dict[str, Path]:
|
420
|
+
"""Create integrated performance dashboard with workflow visualization.
|
421
|
+
|
422
|
+
Args:
|
423
|
+
run_id: Run ID to visualize
|
424
|
+
task_manager: Task manager with execution data
|
425
|
+
output_dir: Directory for output files
|
426
|
+
|
427
|
+
Returns:
|
428
|
+
Dictionary of output file paths
|
429
|
+
"""
|
430
|
+
from kailash.visualization.performance import PerformanceVisualizer
|
431
|
+
|
432
|
+
if output_dir is None:
|
433
|
+
output_dir = Path.cwd() / "outputs" / "performance" / run_id
|
434
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
435
|
+
|
436
|
+
outputs = {}
|
437
|
+
|
438
|
+
# Create workflow execution graph
|
439
|
+
outputs["workflow_graph"] = self.create_execution_graph(
|
440
|
+
run_id, task_manager, str(output_dir / "workflow_execution.md")
|
441
|
+
)
|
442
|
+
|
443
|
+
# Create performance visualizations
|
444
|
+
perf_viz = PerformanceVisualizer(task_manager)
|
445
|
+
perf_outputs = perf_viz.create_run_performance_summary(run_id, output_dir)
|
446
|
+
outputs.update(perf_outputs)
|
447
|
+
|
448
|
+
# Create integrated dashboard HTML
|
449
|
+
dashboard_path = output_dir / "dashboard.html"
|
450
|
+
self._create_dashboard_html(run_id, outputs, dashboard_path)
|
451
|
+
outputs["dashboard"] = dashboard_path
|
452
|
+
|
453
|
+
return outputs
|
454
|
+
|
455
|
+
def _create_dashboard_html(
|
456
|
+
self, run_id: str, outputs: Dict[str, Path], dashboard_path: Path
|
457
|
+
) -> None:
|
458
|
+
"""Create HTML dashboard integrating all visualizations."""
|
459
|
+
html_content = f"""
|
460
|
+
<!DOCTYPE html>
|
461
|
+
<html>
|
462
|
+
<head>
|
463
|
+
<title>Performance Dashboard - Run {run_id}</title>
|
464
|
+
<style>
|
465
|
+
body {{
|
466
|
+
font-family: Arial, sans-serif;
|
467
|
+
margin: 20px;
|
468
|
+
background-color: #f5f5f5;
|
469
|
+
}}
|
470
|
+
.container {{
|
471
|
+
max-width: 1200px;
|
472
|
+
margin: 0 auto;
|
473
|
+
background-color: white;
|
474
|
+
padding: 20px;
|
475
|
+
border-radius: 8px;
|
476
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
477
|
+
}}
|
478
|
+
h1, h2 {{
|
479
|
+
color: #333;
|
480
|
+
}}
|
481
|
+
.section {{
|
482
|
+
margin: 20px 0;
|
483
|
+
padding: 20px;
|
484
|
+
border: 1px solid #ddd;
|
485
|
+
border-radius: 4px;
|
486
|
+
}}
|
487
|
+
.image-grid {{
|
488
|
+
display: grid;
|
489
|
+
grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
|
490
|
+
gap: 20px;
|
491
|
+
margin: 20px 0;
|
492
|
+
}}
|
493
|
+
img {{
|
494
|
+
max-width: 100%;
|
495
|
+
height: auto;
|
496
|
+
border: 1px solid #ddd;
|
497
|
+
border-radius: 4px;
|
498
|
+
}}
|
499
|
+
.report-link {{
|
500
|
+
display: inline-block;
|
501
|
+
margin: 10px 0;
|
502
|
+
padding: 10px 20px;
|
503
|
+
background-color: #007bff;
|
504
|
+
color: white;
|
505
|
+
text-decoration: none;
|
506
|
+
border-radius: 4px;
|
507
|
+
}}
|
508
|
+
.report-link:hover {{
|
509
|
+
background-color: #0056b3;
|
510
|
+
}}
|
511
|
+
</style>
|
512
|
+
</head>
|
513
|
+
<body>
|
514
|
+
<div class="container">
|
515
|
+
<h1>Performance Dashboard - Run {run_id}</h1>
|
516
|
+
|
517
|
+
<div class="section">
|
518
|
+
<h2>Workflow Execution</h2>
|
519
|
+
<p>View the workflow execution graph and task status:</p>
|
520
|
+
<a href="{outputs.get('workflow_graph', '#')}" class="report-link">
|
521
|
+
View Workflow Graph
|
522
|
+
</a>
|
523
|
+
</div>
|
524
|
+
|
525
|
+
<div class="section">
|
526
|
+
<h2>Performance Metrics</h2>
|
527
|
+
<div class="image-grid">
|
528
|
+
"""
|
529
|
+
|
530
|
+
# Add performance images
|
531
|
+
image_keys = [
|
532
|
+
"execution_timeline",
|
533
|
+
"resource_usage",
|
534
|
+
"performance_comparison",
|
535
|
+
"io_analysis",
|
536
|
+
"performance_heatmap",
|
537
|
+
]
|
538
|
+
|
539
|
+
for key in image_keys:
|
540
|
+
if key in outputs:
|
541
|
+
title = key.replace("_", " ").title()
|
542
|
+
html_content += f"""
|
543
|
+
<div>
|
544
|
+
<h3>{title}</h3>
|
545
|
+
<img src="{outputs[key].name}" alt="{title}">
|
546
|
+
</div>
|
547
|
+
"""
|
548
|
+
|
549
|
+
html_content += """
|
550
|
+
</div>
|
551
|
+
</div>
|
552
|
+
|
553
|
+
<div class="section">
|
554
|
+
<h2>Detailed Report</h2>
|
555
|
+
<p>View the comprehensive performance analysis report:</p>
|
556
|
+
<a href="{outputs.get('report', '#')}" class="report-link">
|
557
|
+
View Performance Report
|
558
|
+
</a>
|
559
|
+
</div>
|
560
|
+
</div>
|
561
|
+
</body>
|
562
|
+
</html>
|
563
|
+
"""
|
564
|
+
|
565
|
+
with open(dashboard_path, "w") as f:
|
566
|
+
f.write(html_content)
|
567
|
+
|
568
|
+
|
569
|
+
# Add visualization method to Workflow class
|
570
|
+
def add_visualization_to_workflow():
|
571
|
+
"""Add visualization method to Workflow class."""
|
572
|
+
|
573
|
+
def visualize(self, output_path: Optional[str] = None, **kwargs) -> None:
|
574
|
+
"""Visualize the workflow.
|
575
|
+
|
576
|
+
Args:
|
577
|
+
output_path: Path to save the visualization
|
578
|
+
**kwargs: Additional arguments for the visualizer
|
579
|
+
"""
|
580
|
+
visualizer = WorkflowVisualizer(self)
|
581
|
+
visualizer.visualize(output_path, **kwargs)
|
582
|
+
|
583
|
+
# Add method to Workflow class
|
584
|
+
Workflow.visualize = visualize
|
585
|
+
|
586
|
+
|
587
|
+
# Call this when module is imported
|
588
|
+
add_visualization_to_workflow()
|