kailash 0.8.4__py3-none-any.whl → 0.8.6__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 +5 -11
- kailash/channels/__init__.py +2 -1
- kailash/channels/mcp_channel.py +23 -4
- kailash/cli/__init__.py +11 -1
- kailash/cli/validate_imports.py +202 -0
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/bulkhead.py +15 -5
- kailash/core/resilience/circuit_breaker.py +74 -1
- kailash/core/resilience/health_monitor.py +433 -33
- kailash/edge/compliance.py +33 -0
- kailash/edge/consistency.py +609 -0
- kailash/edge/coordination/__init__.py +30 -0
- kailash/edge/coordination/global_ordering.py +355 -0
- kailash/edge/coordination/leader_election.py +217 -0
- kailash/edge/coordination/partition_detector.py +296 -0
- kailash/edge/coordination/raft.py +485 -0
- kailash/edge/discovery.py +63 -1
- kailash/edge/migration/__init__.py +19 -0
- kailash/edge/migration/edge_migration_service.py +384 -0
- kailash/edge/migration/edge_migrator.py +832 -0
- kailash/edge/monitoring/__init__.py +21 -0
- kailash/edge/monitoring/edge_monitor.py +736 -0
- kailash/edge/prediction/__init__.py +10 -0
- kailash/edge/prediction/predictive_warmer.py +591 -0
- kailash/edge/resource/__init__.py +102 -0
- kailash/edge/resource/cloud_integration.py +796 -0
- kailash/edge/resource/cost_optimizer.py +949 -0
- kailash/edge/resource/docker_integration.py +919 -0
- kailash/edge/resource/kubernetes_integration.py +893 -0
- kailash/edge/resource/platform_integration.py +913 -0
- kailash/edge/resource/predictive_scaler.py +959 -0
- kailash/edge/resource/resource_analyzer.py +824 -0
- kailash/edge/resource/resource_pools.py +610 -0
- kailash/integrations/dataflow_edge.py +261 -0
- kailash/mcp_server/registry_integration.py +1 -1
- kailash/mcp_server/server.py +351 -8
- kailash/mcp_server/transports.py +305 -0
- kailash/middleware/gateway/event_store.py +1 -0
- kailash/monitoring/__init__.py +18 -0
- kailash/monitoring/alerts.py +646 -0
- kailash/monitoring/metrics.py +677 -0
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/ai/semantic_memory.py +2 -2
- kailash/nodes/base.py +622 -1
- kailash/nodes/code/python.py +44 -3
- kailash/nodes/data/async_sql.py +42 -20
- kailash/nodes/edge/__init__.py +36 -0
- kailash/nodes/edge/base.py +240 -0
- kailash/nodes/edge/cloud_node.py +710 -0
- kailash/nodes/edge/coordination.py +239 -0
- kailash/nodes/edge/docker_node.py +825 -0
- kailash/nodes/edge/edge_data.py +582 -0
- kailash/nodes/edge/edge_migration_node.py +396 -0
- kailash/nodes/edge/edge_monitoring_node.py +421 -0
- kailash/nodes/edge/edge_state.py +673 -0
- kailash/nodes/edge/edge_warming_node.py +393 -0
- kailash/nodes/edge/kubernetes_node.py +652 -0
- kailash/nodes/edge/platform_node.py +766 -0
- kailash/nodes/edge/resource_analyzer_node.py +378 -0
- kailash/nodes/edge/resource_optimizer_node.py +501 -0
- kailash/nodes/edge/resource_scaler_node.py +397 -0
- kailash/nodes/governance.py +410 -0
- kailash/nodes/ports.py +676 -0
- kailash/nodes/rag/registry.py +1 -1
- kailash/nodes/transaction/distributed_transaction_manager.py +48 -1
- kailash/nodes/transaction/saga_state_storage.py +2 -1
- kailash/nodes/validation.py +8 -8
- kailash/runtime/local.py +374 -1
- kailash/runtime/validation/__init__.py +12 -0
- kailash/runtime/validation/connection_context.py +119 -0
- kailash/runtime/validation/enhanced_error_formatter.py +202 -0
- kailash/runtime/validation/error_categorizer.py +164 -0
- kailash/runtime/validation/import_validator.py +446 -0
- kailash/runtime/validation/metrics.py +380 -0
- kailash/runtime/validation/performance.py +615 -0
- kailash/runtime/validation/suggestion_engine.py +212 -0
- kailash/testing/fixtures.py +2 -2
- kailash/utils/data_paths.py +74 -0
- kailash/workflow/builder.py +413 -8
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/mermaid_visualizer.py +3 -1
- kailash/workflow/migration.py +3 -3
- kailash/workflow/templates.py +6 -6
- kailash/workflow/type_inference.py +669 -0
- kailash/workflow/validation.py +134 -3
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/METADATA +52 -34
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/RECORD +93 -42
- kailash/nexus/__init__.py +0 -21
- kailash/nexus/cli/__init__.py +0 -5
- kailash/nexus/cli/__main__.py +0 -6
- kailash/nexus/cli/main.py +0 -176
- kailash/nexus/factory.py +0 -413
- kailash/nexus/gateway.py +0 -545
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/WHEEL +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,570 @@
|
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
Workflow Validation Audit Tool
|
4
|
+
|
5
|
+
A CLI tool for auditing workflow connections and generating migration reports
|
6
|
+
to help users transition to strict connection validation mode.
|
7
|
+
|
8
|
+
Usage:
|
9
|
+
python -m kailash.cli.validation_audit <workflow_file> [options]
|
10
|
+
|
11
|
+
Options:
|
12
|
+
--format Output format: text, json, csv (default: text)
|
13
|
+
--output Output file path (default: stdout)
|
14
|
+
--mode Validation mode to test: off, warn, strict (default: strict)
|
15
|
+
--detailed Include detailed validation results
|
16
|
+
--fix-suggestions Show suggestions for fixing validation issues
|
17
|
+
"""
|
18
|
+
|
19
|
+
import argparse
|
20
|
+
import csv
|
21
|
+
import importlib.machinery
|
22
|
+
import importlib.util
|
23
|
+
import json
|
24
|
+
import os
|
25
|
+
import sys
|
26
|
+
from datetime import datetime
|
27
|
+
from pathlib import Path
|
28
|
+
from typing import Any, Dict, List, Optional, Tuple
|
29
|
+
|
30
|
+
from kailash.nodes import NodeRegistry
|
31
|
+
from kailash.runtime.local import LocalRuntime
|
32
|
+
from kailash.runtime.validation.error_categorizer import ErrorCategorizer
|
33
|
+
from kailash.runtime.validation.metrics import (
|
34
|
+
get_metrics_collector,
|
35
|
+
reset_global_metrics,
|
36
|
+
)
|
37
|
+
from kailash.workflow.builder import WorkflowBuilder
|
38
|
+
from kailash.workflow.graph import Workflow
|
39
|
+
|
40
|
+
|
41
|
+
class ValidationAuditReport:
|
42
|
+
"""Container for validation audit results."""
|
43
|
+
|
44
|
+
def __init__(self, workflow_path: str, validation_mode: str):
|
45
|
+
self.workflow_path = workflow_path
|
46
|
+
self.validation_mode = validation_mode
|
47
|
+
self.timestamp = datetime.now().isoformat()
|
48
|
+
self.total_connections = 0
|
49
|
+
self.passed_connections = []
|
50
|
+
self.failed_connections = []
|
51
|
+
self.security_violations = []
|
52
|
+
self.warnings = []
|
53
|
+
self.suggestions = {}
|
54
|
+
self.performance_metrics = {}
|
55
|
+
|
56
|
+
def add_passed_connection(self, connection_info: Dict[str, Any]) -> None:
|
57
|
+
"""Add a passed connection validation."""
|
58
|
+
self.passed_connections.append(connection_info)
|
59
|
+
|
60
|
+
def add_failed_connection(self, connection_info: Dict[str, Any]) -> None:
|
61
|
+
"""Add a failed connection validation."""
|
62
|
+
self.failed_connections.append(connection_info)
|
63
|
+
|
64
|
+
def add_security_violation(self, violation_info: Dict[str, Any]) -> None:
|
65
|
+
"""Add a security violation."""
|
66
|
+
self.security_violations.append(violation_info)
|
67
|
+
|
68
|
+
def add_warning(self, warning: str) -> None:
|
69
|
+
"""Add a general warning."""
|
70
|
+
self.warnings.append(warning)
|
71
|
+
|
72
|
+
def add_suggestion(self, connection_id: str, suggestion: str) -> None:
|
73
|
+
"""Add a fix suggestion for a connection."""
|
74
|
+
if connection_id not in self.suggestions:
|
75
|
+
self.suggestions[connection_id] = []
|
76
|
+
self.suggestions[connection_id].append(suggestion)
|
77
|
+
|
78
|
+
def get_summary(self) -> Dict[str, Any]:
|
79
|
+
"""Get audit summary statistics."""
|
80
|
+
return {
|
81
|
+
"workflow_path": self.workflow_path,
|
82
|
+
"timestamp": self.timestamp,
|
83
|
+
"validation_mode": self.validation_mode,
|
84
|
+
"total_connections": self.total_connections,
|
85
|
+
"passed": len(self.passed_connections),
|
86
|
+
"failed": len(self.failed_connections),
|
87
|
+
"security_violations": len(self.security_violations),
|
88
|
+
"warnings": len(self.warnings),
|
89
|
+
"pass_rate": (
|
90
|
+
len(self.passed_connections) / self.total_connections * 100
|
91
|
+
if self.total_connections > 0
|
92
|
+
else 0
|
93
|
+
),
|
94
|
+
}
|
95
|
+
|
96
|
+
|
97
|
+
class WorkflowValidationAuditor:
|
98
|
+
"""Audits workflow connections for validation compliance."""
|
99
|
+
|
100
|
+
def __init__(self, validation_mode: str = "strict"):
|
101
|
+
self.validation_mode = validation_mode
|
102
|
+
self.categorizer = ErrorCategorizer()
|
103
|
+
|
104
|
+
def audit_workflow(
|
105
|
+
self, workflow: Workflow, detailed: bool = False
|
106
|
+
) -> ValidationAuditReport:
|
107
|
+
"""Audit a workflow for connection validation compliance.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
workflow: The workflow to audit
|
111
|
+
detailed: Whether to include detailed validation results
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
ValidationAuditReport with audit results
|
115
|
+
"""
|
116
|
+
reset_global_metrics() # Start fresh
|
117
|
+
|
118
|
+
report = ValidationAuditReport(
|
119
|
+
workflow_path=getattr(workflow, "_source_file", "unknown"),
|
120
|
+
validation_mode=self.validation_mode,
|
121
|
+
)
|
122
|
+
|
123
|
+
# Count total connections
|
124
|
+
report.total_connections = len(workflow.connections)
|
125
|
+
|
126
|
+
# Create a test runtime with the specified validation mode
|
127
|
+
runtime = LocalRuntime(connection_validation=self.validation_mode)
|
128
|
+
|
129
|
+
# Perform dry run to collect validation results
|
130
|
+
try:
|
131
|
+
# Execute workflow in dry-run mode (if supported) or with minimal data
|
132
|
+
results, run_id = runtime.execute(workflow, parameters={})
|
133
|
+
|
134
|
+
# If execution succeeds, all connections passed validation
|
135
|
+
for connection in workflow.connections:
|
136
|
+
conn_id = f"{connection.source_node}.{connection.source_output} → {connection.target_node}.{connection.target_input}"
|
137
|
+
report.add_passed_connection(
|
138
|
+
{
|
139
|
+
"id": conn_id,
|
140
|
+
"source": connection.source_node,
|
141
|
+
"source_port": connection.source_output,
|
142
|
+
"target": connection.target_node,
|
143
|
+
"target_port": connection.target_input,
|
144
|
+
"status": "passed",
|
145
|
+
}
|
146
|
+
)
|
147
|
+
|
148
|
+
except Exception as e:
|
149
|
+
# Execution failed - analyze the error
|
150
|
+
self._analyze_validation_failure(e, workflow, report, detailed)
|
151
|
+
|
152
|
+
# Get metrics from the runtime
|
153
|
+
metrics = runtime.get_validation_metrics()
|
154
|
+
report.performance_metrics = metrics["performance_summary"]
|
155
|
+
|
156
|
+
# Add security violations from metrics
|
157
|
+
security_report = metrics["security_report"]
|
158
|
+
for violation in security_report.get("most_recent_violations", []):
|
159
|
+
report.add_security_violation(violation)
|
160
|
+
|
161
|
+
# Add suggestions for failed connections
|
162
|
+
self._generate_fix_suggestions(report)
|
163
|
+
|
164
|
+
return report
|
165
|
+
|
166
|
+
def _analyze_validation_failure(
|
167
|
+
self,
|
168
|
+
error: Exception,
|
169
|
+
workflow: Workflow,
|
170
|
+
report: ValidationAuditReport,
|
171
|
+
detailed: bool,
|
172
|
+
) -> None:
|
173
|
+
"""Analyze validation failure and populate report."""
|
174
|
+
error_msg = str(error)
|
175
|
+
|
176
|
+
# Try to extract connection information from error
|
177
|
+
for connection in workflow.connections:
|
178
|
+
conn_id = f"{connection.source_node}.{connection.source_output} → {connection.target_node}.{connection.target_input}"
|
179
|
+
|
180
|
+
# Simple heuristic: if connection nodes mentioned in error, it likely failed
|
181
|
+
if (
|
182
|
+
connection.source_node in error_msg
|
183
|
+
or connection.target_node in error_msg
|
184
|
+
):
|
185
|
+
|
186
|
+
# Categorize the error
|
187
|
+
# Get node type from workflow nodes
|
188
|
+
target_node = workflow.nodes.get(connection.target_node)
|
189
|
+
node_type = target_node.node_type if target_node else "Unknown"
|
190
|
+
|
191
|
+
error_category = self.categorizer.categorize_error(error, node_type)
|
192
|
+
|
193
|
+
failure_info = {
|
194
|
+
"id": conn_id,
|
195
|
+
"source": connection.source_node,
|
196
|
+
"source_port": connection.source_output,
|
197
|
+
"target": connection.target_node,
|
198
|
+
"target_port": connection.target_input,
|
199
|
+
"status": "failed",
|
200
|
+
"error": error_msg if detailed else "Validation failed",
|
201
|
+
"category": error_category.value,
|
202
|
+
}
|
203
|
+
|
204
|
+
report.add_failed_connection(failure_info)
|
205
|
+
else:
|
206
|
+
# Assume passed if not mentioned in error
|
207
|
+
report.add_passed_connection(
|
208
|
+
{
|
209
|
+
"id": conn_id,
|
210
|
+
"source": connection.source_node,
|
211
|
+
"source_port": connection.source_output,
|
212
|
+
"target": connection.target_node,
|
213
|
+
"target_port": connection.target_input,
|
214
|
+
"status": "passed",
|
215
|
+
}
|
216
|
+
)
|
217
|
+
|
218
|
+
def _generate_fix_suggestions(self, report: ValidationAuditReport) -> None:
|
219
|
+
"""Generate fix suggestions for failed connections."""
|
220
|
+
for failed in report.failed_connections:
|
221
|
+
conn_id = failed["id"]
|
222
|
+
category = failed.get("category", "unknown")
|
223
|
+
|
224
|
+
if category == "type_mismatch":
|
225
|
+
report.add_suggestion(
|
226
|
+
conn_id,
|
227
|
+
"Add a transformation node between source and target to convert data types",
|
228
|
+
)
|
229
|
+
report.add_suggestion(
|
230
|
+
conn_id,
|
231
|
+
"Check if you're using the correct output port from the source node",
|
232
|
+
)
|
233
|
+
|
234
|
+
elif category == "missing_parameter":
|
235
|
+
report.add_suggestion(
|
236
|
+
conn_id,
|
237
|
+
"Ensure all required parameters are provided via connections or node config",
|
238
|
+
)
|
239
|
+
report.add_suggestion(
|
240
|
+
conn_id,
|
241
|
+
"Add the missing connection or provide default value in node configuration",
|
242
|
+
)
|
243
|
+
|
244
|
+
elif category == "security_violation":
|
245
|
+
report.add_suggestion(
|
246
|
+
conn_id, "Add input sanitization node before the target node"
|
247
|
+
)
|
248
|
+
report.add_suggestion(
|
249
|
+
conn_id, "Use parameterized queries for database operations"
|
250
|
+
)
|
251
|
+
report.add_suggestion(
|
252
|
+
conn_id, "Implement validation logic in the source node"
|
253
|
+
)
|
254
|
+
|
255
|
+
elif category == "constraint_violation":
|
256
|
+
report.add_suggestion(
|
257
|
+
conn_id, "Add validation node to ensure data meets constraints"
|
258
|
+
)
|
259
|
+
report.add_suggestion(
|
260
|
+
conn_id, "Check node documentation for parameter requirements"
|
261
|
+
)
|
262
|
+
|
263
|
+
|
264
|
+
class ReportFormatter:
|
265
|
+
"""Formats validation audit reports for different output formats."""
|
266
|
+
|
267
|
+
@staticmethod
|
268
|
+
def format_text(report: ValidationAuditReport, detailed: bool = False) -> str:
|
269
|
+
"""Format report as human-readable text."""
|
270
|
+
lines = []
|
271
|
+
summary = report.get_summary()
|
272
|
+
|
273
|
+
# Header
|
274
|
+
lines.append("=" * 70)
|
275
|
+
lines.append("WORKFLOW VALIDATION AUDIT REPORT")
|
276
|
+
lines.append("=" * 70)
|
277
|
+
lines.append(f"Workflow: {summary['workflow_path']}")
|
278
|
+
lines.append(f"Timestamp: {summary['timestamp']}")
|
279
|
+
lines.append(f"Validation Mode: {summary['validation_mode']}")
|
280
|
+
lines.append("")
|
281
|
+
|
282
|
+
# Summary
|
283
|
+
lines.append("SUMMARY")
|
284
|
+
lines.append("-" * 30)
|
285
|
+
lines.append(f"Total Connections: {summary['total_connections']}")
|
286
|
+
lines.append(f"Passed: {summary['passed']} ({summary['pass_rate']:.1f}%)")
|
287
|
+
lines.append(f"Failed: {summary['failed']}")
|
288
|
+
lines.append(f"Security Violations: {summary['security_violations']}")
|
289
|
+
lines.append("")
|
290
|
+
|
291
|
+
# Failed connections
|
292
|
+
if report.failed_connections:
|
293
|
+
lines.append("FAILED CONNECTIONS")
|
294
|
+
lines.append("-" * 30)
|
295
|
+
for failed in report.failed_connections:
|
296
|
+
lines.append(f"❌ {failed['id']}")
|
297
|
+
lines.append(f" Category: {failed.get('category', 'unknown')}")
|
298
|
+
if detailed and "error" in failed:
|
299
|
+
lines.append(f" Error: {failed['error']}")
|
300
|
+
|
301
|
+
# Add suggestions
|
302
|
+
conn_id = failed["id"]
|
303
|
+
if conn_id in report.suggestions:
|
304
|
+
lines.append(" Suggestions:")
|
305
|
+
for suggestion in report.suggestions[conn_id]:
|
306
|
+
lines.append(f" • {suggestion}")
|
307
|
+
lines.append("")
|
308
|
+
|
309
|
+
# Security violations
|
310
|
+
if report.security_violations:
|
311
|
+
lines.append("SECURITY VIOLATIONS")
|
312
|
+
lines.append("-" * 30)
|
313
|
+
for violation in report.security_violations:
|
314
|
+
lines.append(f"🔒 {violation.get('node', 'Unknown node')}")
|
315
|
+
lines.append(
|
316
|
+
f" {violation.get('details', {}).get('message', 'Security issue detected')}"
|
317
|
+
)
|
318
|
+
lines.append("")
|
319
|
+
|
320
|
+
# Warnings
|
321
|
+
if report.warnings:
|
322
|
+
lines.append("WARNINGS")
|
323
|
+
lines.append("-" * 30)
|
324
|
+
for warning in report.warnings:
|
325
|
+
lines.append(f"⚠️ {warning}")
|
326
|
+
lines.append("")
|
327
|
+
|
328
|
+
# Performance metrics
|
329
|
+
if report.performance_metrics:
|
330
|
+
lines.append("PERFORMANCE METRICS")
|
331
|
+
lines.append("-" * 30)
|
332
|
+
metrics = report.performance_metrics
|
333
|
+
if "performance_by_node_type" in metrics:
|
334
|
+
for node_type, perf in metrics["performance_by_node_type"].items():
|
335
|
+
lines.append(f"{node_type}:")
|
336
|
+
lines.append(f" Average: {perf['avg_ms']:.2f}ms")
|
337
|
+
lines.append(
|
338
|
+
f" Min: {perf['min_ms']:.2f}ms, Max: {perf['max_ms']:.2f}ms"
|
339
|
+
)
|
340
|
+
lines.append("")
|
341
|
+
|
342
|
+
# Migration recommendation
|
343
|
+
lines.append("MIGRATION RECOMMENDATION")
|
344
|
+
lines.append("-" * 30)
|
345
|
+
if summary["failed"] == 0 and summary["security_violations"] == 0:
|
346
|
+
lines.append("✅ This workflow is ready for strict validation mode!")
|
347
|
+
lines.append(" You can safely enable connection_validation='strict'")
|
348
|
+
else:
|
349
|
+
lines.append(
|
350
|
+
"❗ This workflow needs updates before enabling strict validation:"
|
351
|
+
)
|
352
|
+
lines.append(f" - Fix {summary['failed']} failed connections")
|
353
|
+
if summary["security_violations"] > 0:
|
354
|
+
lines.append(
|
355
|
+
f" - Address {summary['security_violations']} security violations"
|
356
|
+
)
|
357
|
+
lines.append(" - Review the suggestions above for each issue")
|
358
|
+
|
359
|
+
return "\n".join(lines)
|
360
|
+
|
361
|
+
@staticmethod
|
362
|
+
def format_json(report: ValidationAuditReport, detailed: bool = False) -> str:
|
363
|
+
"""Format report as JSON."""
|
364
|
+
data = report.get_summary()
|
365
|
+
data["passed_connections"] = report.passed_connections
|
366
|
+
data["failed_connections"] = report.failed_connections
|
367
|
+
data["security_violations"] = report.security_violations
|
368
|
+
data["warnings"] = report.warnings
|
369
|
+
data["suggestions"] = report.suggestions
|
370
|
+
|
371
|
+
if detailed:
|
372
|
+
data["performance_metrics"] = report.performance_metrics
|
373
|
+
|
374
|
+
return json.dumps(data, indent=2)
|
375
|
+
|
376
|
+
@staticmethod
|
377
|
+
def format_csv(report: ValidationAuditReport, detailed: bool = False) -> str:
|
378
|
+
"""Format report as CSV."""
|
379
|
+
output = []
|
380
|
+
|
381
|
+
# Header
|
382
|
+
headers = [
|
383
|
+
"Connection ID",
|
384
|
+
"Source",
|
385
|
+
"Source Port",
|
386
|
+
"Target",
|
387
|
+
"Target Port",
|
388
|
+
"Status",
|
389
|
+
"Category",
|
390
|
+
"Suggestions",
|
391
|
+
]
|
392
|
+
if detailed:
|
393
|
+
headers.append("Error")
|
394
|
+
|
395
|
+
# Use string buffer for CSV
|
396
|
+
import io
|
397
|
+
|
398
|
+
buffer = io.StringIO()
|
399
|
+
writer = csv.writer(buffer)
|
400
|
+
writer.writerow(headers)
|
401
|
+
|
402
|
+
# All connections
|
403
|
+
all_connections = []
|
404
|
+
|
405
|
+
# Add passed connections
|
406
|
+
for conn in report.passed_connections:
|
407
|
+
row = [
|
408
|
+
conn["id"],
|
409
|
+
conn["source"],
|
410
|
+
conn["source_port"],
|
411
|
+
conn["target"],
|
412
|
+
conn["target_port"],
|
413
|
+
"PASSED",
|
414
|
+
"",
|
415
|
+
"",
|
416
|
+
]
|
417
|
+
if detailed:
|
418
|
+
row.append("")
|
419
|
+
writer.writerow(row)
|
420
|
+
|
421
|
+
# Add failed connections
|
422
|
+
for conn in report.failed_connections:
|
423
|
+
suggestions = "; ".join(report.suggestions.get(conn["id"], []))
|
424
|
+
row = [
|
425
|
+
conn["id"],
|
426
|
+
conn["source"],
|
427
|
+
conn["source_port"],
|
428
|
+
conn["target"],
|
429
|
+
conn["target_port"],
|
430
|
+
"FAILED",
|
431
|
+
conn.get("category", "unknown"),
|
432
|
+
suggestions,
|
433
|
+
]
|
434
|
+
if detailed:
|
435
|
+
row.append(conn.get("error", ""))
|
436
|
+
writer.writerow(row)
|
437
|
+
|
438
|
+
return buffer.getvalue()
|
439
|
+
|
440
|
+
|
441
|
+
def load_workflow_from_file(file_path: str) -> Workflow:
|
442
|
+
"""Load a workflow from a Python file.
|
443
|
+
|
444
|
+
Args:
|
445
|
+
file_path: Path to the workflow file
|
446
|
+
|
447
|
+
Returns:
|
448
|
+
Loaded Workflow object
|
449
|
+
|
450
|
+
Raises:
|
451
|
+
ValueError: If workflow cannot be loaded
|
452
|
+
"""
|
453
|
+
file_path = Path(file_path).resolve()
|
454
|
+
|
455
|
+
if file_path.suffix != ".py":
|
456
|
+
raise ValueError(f"Workflow file must be a Python file (.py): {file_path}")
|
457
|
+
|
458
|
+
if not file_path.exists():
|
459
|
+
raise ValueError(f"Workflow file not found: {file_path}")
|
460
|
+
|
461
|
+
# Load the module
|
462
|
+
spec = importlib.util.spec_from_file_location("workflow_module", file_path)
|
463
|
+
if spec is None or spec.loader is None:
|
464
|
+
raise ValueError(f"Could not load workflow from: {file_path}")
|
465
|
+
|
466
|
+
module = importlib.util.module_from_spec(spec)
|
467
|
+
sys.modules["workflow_module"] = module
|
468
|
+
spec.loader.exec_module(module)
|
469
|
+
|
470
|
+
# Find workflow in module
|
471
|
+
workflow = None
|
472
|
+
|
473
|
+
# Look for common patterns
|
474
|
+
if hasattr(module, "workflow"):
|
475
|
+
workflow = module.workflow
|
476
|
+
elif hasattr(module, "build_workflow"):
|
477
|
+
workflow = module.build_workflow()
|
478
|
+
elif hasattr(module, "create_workflow"):
|
479
|
+
workflow = module.create_workflow()
|
480
|
+
else:
|
481
|
+
# Look for any Workflow or WorkflowBuilder instance
|
482
|
+
for name, obj in vars(module).items():
|
483
|
+
if isinstance(obj, Workflow):
|
484
|
+
workflow = obj
|
485
|
+
break
|
486
|
+
elif isinstance(obj, WorkflowBuilder):
|
487
|
+
workflow = obj.build()
|
488
|
+
break
|
489
|
+
|
490
|
+
if workflow is None:
|
491
|
+
raise ValueError(
|
492
|
+
f"No workflow found in {file_path}. "
|
493
|
+
"Expected 'workflow' variable or 'build_workflow()' function."
|
494
|
+
)
|
495
|
+
|
496
|
+
# Store source file for reporting
|
497
|
+
workflow._source_file = str(file_path)
|
498
|
+
|
499
|
+
return workflow
|
500
|
+
|
501
|
+
|
502
|
+
def main():
|
503
|
+
"""Main CLI entry point."""
|
504
|
+
parser = argparse.ArgumentParser(
|
505
|
+
description="Audit workflow connections for validation compliance"
|
506
|
+
)
|
507
|
+
|
508
|
+
parser.add_argument("workflow_file", help="Path to the workflow Python file")
|
509
|
+
|
510
|
+
parser.add_argument(
|
511
|
+
"--format",
|
512
|
+
choices=["text", "json", "csv"],
|
513
|
+
default="text",
|
514
|
+
help="Output format (default: text)",
|
515
|
+
)
|
516
|
+
|
517
|
+
parser.add_argument("--output", help="Output file path (default: stdout)")
|
518
|
+
|
519
|
+
parser.add_argument(
|
520
|
+
"--mode",
|
521
|
+
choices=["off", "warn", "strict"],
|
522
|
+
default="strict",
|
523
|
+
help="Validation mode to test (default: strict)",
|
524
|
+
)
|
525
|
+
|
526
|
+
parser.add_argument(
|
527
|
+
"--detailed", action="store_true", help="Include detailed validation results"
|
528
|
+
)
|
529
|
+
|
530
|
+
parser.add_argument(
|
531
|
+
"--fix-suggestions",
|
532
|
+
action="store_true",
|
533
|
+
help="Show suggestions for fixing validation issues",
|
534
|
+
)
|
535
|
+
|
536
|
+
args = parser.parse_args()
|
537
|
+
|
538
|
+
try:
|
539
|
+
# Load workflow
|
540
|
+
workflow = load_workflow_from_file(args.workflow_file)
|
541
|
+
|
542
|
+
# Audit workflow
|
543
|
+
auditor = WorkflowValidationAuditor(validation_mode=args.mode)
|
544
|
+
report = auditor.audit_workflow(workflow, detailed=args.detailed)
|
545
|
+
|
546
|
+
# Format report
|
547
|
+
if args.format == "json":
|
548
|
+
output = ReportFormatter.format_json(report, args.detailed)
|
549
|
+
elif args.format == "csv":
|
550
|
+
output = ReportFormatter.format_csv(report, args.detailed)
|
551
|
+
else:
|
552
|
+
output = ReportFormatter.format_text(
|
553
|
+
report, args.detailed or args.fix_suggestions
|
554
|
+
)
|
555
|
+
|
556
|
+
# Output report
|
557
|
+
if args.output:
|
558
|
+
with open(args.output, "w") as f:
|
559
|
+
f.write(output)
|
560
|
+
print(f"Report saved to: {args.output}")
|
561
|
+
else:
|
562
|
+
print(output)
|
563
|
+
|
564
|
+
except Exception as e:
|
565
|
+
print(f"Error: {e}", file=sys.stderr)
|
566
|
+
sys.exit(1)
|
567
|
+
|
568
|
+
|
569
|
+
if __name__ == "__main__":
|
570
|
+
main()
|
@@ -200,7 +200,7 @@ class ActorSupervisor:
|
|
200
200
|
"""Monitor actor health periodically."""
|
201
201
|
while self._running:
|
202
202
|
try:
|
203
|
-
await asyncio.sleep(
|
203
|
+
await asyncio.sleep(0.1) # Fast health checks for tests
|
204
204
|
|
205
205
|
for actor_id, actor in list(self.actors.items()):
|
206
206
|
if actor.state == ConnectionState.FAILED:
|
@@ -204,11 +204,21 @@ class BulkheadPartition:
|
|
204
204
|
await self._record_failure(execution_time)
|
205
205
|
raise
|
206
206
|
finally:
|
207
|
-
# Clean up
|
208
|
-
|
209
|
-
|
210
|
-
self._active_operations
|
211
|
-
|
207
|
+
# Clean up - with proper exception handling for event loop issues
|
208
|
+
try:
|
209
|
+
async with self._lock:
|
210
|
+
if operation_id in self._active_operations:
|
211
|
+
self._active_operations.remove(operation_id)
|
212
|
+
self.metrics.active_operations = len(self._active_operations)
|
213
|
+
except (RuntimeError, asyncio.CancelledError):
|
214
|
+
# Handle event loop issues during cleanup - force cleanup without lock
|
215
|
+
try:
|
216
|
+
if operation_id in self._active_operations:
|
217
|
+
self._active_operations.remove(operation_id)
|
218
|
+
self.metrics.active_operations = len(self._active_operations)
|
219
|
+
except:
|
220
|
+
# Final fallback - ignore cleanup errors during shutdown
|
221
|
+
pass
|
212
222
|
|
213
223
|
async def _execute_isolated(
|
214
224
|
self, operation_id: str, func: Callable, args: tuple, kwargs: dict, timeout: int
|