kailash 0.8.4__py3-none-any.whl → 0.8.5__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 +1 -7
- kailash/cli/__init__.py +11 -1
- kailash/cli/validation_audit.py +570 -0
- kailash/core/actors/supervisor.py +1 -1
- kailash/core/resilience/circuit_breaker.py +71 -1
- kailash/core/resilience/health_monitor.py +172 -0
- 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_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/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 +545 -0
- 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 +392 -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/ports.py +676 -0
- kailash/runtime/local.py +344 -1
- kailash/runtime/validation/__init__.py +20 -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/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/workflow/builder.py +230 -4
- kailash/workflow/contracts.py +418 -0
- kailash/workflow/edge_infrastructure.py +369 -0
- kailash/workflow/migration.py +3 -3
- kailash/workflow/type_inference.py +669 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/METADATA +43 -27
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/RECORD +73 -27
- 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.5.dist-info}/WHEEL +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.4.dist-info → kailash-0.8.5.dist-info}/top_level.txt +0 -0
@@ -396,6 +396,16 @@ class ConnectionCircuitBreaker(Generic[T]):
|
|
396
396
|
if listener in self._listeners:
|
397
397
|
self._listeners.remove(listener)
|
398
398
|
|
399
|
+
@property
|
400
|
+
def success_count(self) -> int:
|
401
|
+
"""Get number of successful calls."""
|
402
|
+
return self.metrics.successful_calls
|
403
|
+
|
404
|
+
@property
|
405
|
+
def failure_count(self) -> int:
|
406
|
+
"""Get number of failed calls."""
|
407
|
+
return self.metrics.failed_calls
|
408
|
+
|
399
409
|
def get_status(self) -> Dict[str, Any]:
|
400
410
|
"""Get current circuit breaker status."""
|
401
411
|
return {
|
@@ -435,10 +445,16 @@ class ConnectionCircuitBreaker(Generic[T]):
|
|
435
445
|
class CircuitBreakerManager:
|
436
446
|
"""Manages multiple circuit breakers for different resources."""
|
437
447
|
|
438
|
-
def __init__(self):
|
448
|
+
def __init__(self, performance_monitor=None):
|
439
449
|
"""Initialize circuit breaker manager."""
|
440
450
|
self._breakers: Dict[str, ConnectionCircuitBreaker] = {}
|
441
451
|
self._default_config = CircuitBreakerConfig()
|
452
|
+
self._performance_monitor = performance_monitor
|
453
|
+
self._patterns = {
|
454
|
+
"database": CircuitBreakerConfig(failure_threshold=5, recovery_timeout=60),
|
455
|
+
"api": CircuitBreakerConfig(failure_threshold=3, recovery_timeout=30),
|
456
|
+
"cache": CircuitBreakerConfig(failure_threshold=2, recovery_timeout=15),
|
457
|
+
}
|
442
458
|
|
443
459
|
def get_or_create(
|
444
460
|
self, name: str, config: Optional[CircuitBreakerConfig] = None
|
@@ -450,6 +466,60 @@ class CircuitBreakerManager:
|
|
450
466
|
)
|
451
467
|
return self._breakers[name]
|
452
468
|
|
469
|
+
def create_circuit_breaker(
|
470
|
+
self, name: str, config: Optional[CircuitBreakerConfig] = None, pattern: Optional[str] = None
|
471
|
+
) -> ConnectionCircuitBreaker:
|
472
|
+
"""Create a new circuit breaker with optional pattern-based configuration."""
|
473
|
+
if pattern and pattern in self._patterns:
|
474
|
+
config = config or self._patterns[pattern]
|
475
|
+
return self.get_or_create(name, config)
|
476
|
+
|
477
|
+
async def execute_with_circuit_breaker(
|
478
|
+
self, name: str, func: Callable, fallback: Optional[Callable] = None
|
479
|
+
):
|
480
|
+
"""Execute a function with circuit breaker protection."""
|
481
|
+
cb = self.get_or_create(name)
|
482
|
+
try:
|
483
|
+
result = await cb.call(func)
|
484
|
+
return result
|
485
|
+
except CircuitBreakerError:
|
486
|
+
if fallback:
|
487
|
+
if asyncio.iscoroutinefunction(fallback):
|
488
|
+
return await fallback()
|
489
|
+
else:
|
490
|
+
return fallback()
|
491
|
+
raise
|
492
|
+
|
493
|
+
def get_circuit_breaker(self, name: str) -> Optional[ConnectionCircuitBreaker]:
|
494
|
+
"""Get an existing circuit breaker by name."""
|
495
|
+
return self._breakers.get(name)
|
496
|
+
|
497
|
+
def get_all_circuit_states(self) -> Dict[str, Dict[str, Any]]:
|
498
|
+
"""Get the state of all circuit breakers."""
|
499
|
+
return {name: cb.get_status() for name, cb in self._breakers.items()}
|
500
|
+
|
501
|
+
def force_open_circuit_breaker(self, name: str) -> bool:
|
502
|
+
"""Manually open a circuit breaker."""
|
503
|
+
cb = self._breakers.get(name)
|
504
|
+
if cb:
|
505
|
+
asyncio.create_task(cb.force_open("Manual override"))
|
506
|
+
return True
|
507
|
+
return False
|
508
|
+
|
509
|
+
def reset_circuit_breaker(self, name: str) -> bool:
|
510
|
+
"""Reset a circuit breaker to closed state."""
|
511
|
+
cb = self._breakers.get(name)
|
512
|
+
if cb:
|
513
|
+
asyncio.create_task(cb.reset())
|
514
|
+
return True
|
515
|
+
return False
|
516
|
+
|
517
|
+
def add_global_callback(self, callback: Callable):
|
518
|
+
"""Add a global callback for circuit breaker state changes."""
|
519
|
+
# For now, add to all existing breakers
|
520
|
+
for cb in self._breakers.values():
|
521
|
+
cb.add_listener(callback)
|
522
|
+
|
453
523
|
def get_all_status(self) -> Dict[str, Dict[str, Any]]:
|
454
524
|
"""Get status of all circuit breakers."""
|
455
525
|
return {name: breaker.get_status() for name, breaker in self._breakers.items()}
|
@@ -576,3 +576,175 @@ async def quick_health_check(service_name: str) -> bool:
|
|
576
576
|
return result.is_healthy if result else False
|
577
577
|
except Exception:
|
578
578
|
return False
|
579
|
+
|
580
|
+
|
581
|
+
@dataclass
|
582
|
+
class HealthSummary:
|
583
|
+
"""Health summary for all checks."""
|
584
|
+
|
585
|
+
total_checks: int
|
586
|
+
healthy_checks: int
|
587
|
+
degraded_checks: int
|
588
|
+
unhealthy_checks: int
|
589
|
+
overall_status: HealthStatus
|
590
|
+
details: List[HealthCheckResult]
|
591
|
+
|
592
|
+
|
593
|
+
class HealthCheckManager:
|
594
|
+
"""Manager for orchestrating multiple health checks with configuration."""
|
595
|
+
|
596
|
+
def __init__(self, config: Dict[str, Any]):
|
597
|
+
"""Initialize health check manager with configuration."""
|
598
|
+
self.config = config
|
599
|
+
self.enabled = config.get("enabled", True)
|
600
|
+
self.default_interval = config.get("default_interval", 30.0)
|
601
|
+
self.parallel_checks = config.get("parallel_checks", True)
|
602
|
+
self.max_concurrent_checks = config.get("max_concurrent_checks", 10)
|
603
|
+
|
604
|
+
self.health_checks: Dict[str, HealthCheck] = {}
|
605
|
+
self.check_intervals: Dict[str, float] = {}
|
606
|
+
self.last_results: Dict[str, HealthCheckResult] = {}
|
607
|
+
self.history: Dict[str, List[HealthCheckResult]] = {}
|
608
|
+
self.status_change_callbacks: List[Callable] = []
|
609
|
+
self._running = False
|
610
|
+
|
611
|
+
def register_health_check(self, health_check: HealthCheck, interval: float = None):
|
612
|
+
"""Register a health check with optional interval."""
|
613
|
+
check_name = health_check.check_name
|
614
|
+
self.health_checks[check_name] = health_check
|
615
|
+
self.check_intervals[check_name] = interval or self.default_interval
|
616
|
+
self.history[check_name] = []
|
617
|
+
|
618
|
+
async def run_health_check(self, check_name: str) -> HealthCheckResult:
|
619
|
+
"""Run a specific health check."""
|
620
|
+
if check_name not in self.health_checks:
|
621
|
+
raise ValueError(f"Health check '{check_name}' not found")
|
622
|
+
|
623
|
+
health_check = self.health_checks[check_name]
|
624
|
+
result = await health_check.check_health()
|
625
|
+
|
626
|
+
# Store result
|
627
|
+
self.last_results[check_name] = result
|
628
|
+
self.history[check_name].append(result)
|
629
|
+
|
630
|
+
# Check for status changes
|
631
|
+
await self._check_status_change(check_name, result)
|
632
|
+
|
633
|
+
return result
|
634
|
+
|
635
|
+
async def run_all_health_checks(self) -> List[HealthCheckResult]:
|
636
|
+
"""Run all registered health checks."""
|
637
|
+
if not self.health_checks:
|
638
|
+
return []
|
639
|
+
|
640
|
+
if self.parallel_checks:
|
641
|
+
# Run checks in parallel
|
642
|
+
tasks = [
|
643
|
+
self.run_health_check(check_name)
|
644
|
+
for check_name in self.health_checks.keys()
|
645
|
+
]
|
646
|
+
|
647
|
+
# Limit concurrency
|
648
|
+
semaphore = asyncio.Semaphore(self.max_concurrent_checks)
|
649
|
+
|
650
|
+
async def run_with_semaphore(task):
|
651
|
+
async with semaphore:
|
652
|
+
return await task
|
653
|
+
|
654
|
+
results = await asyncio.gather(*[
|
655
|
+
run_with_semaphore(task) for task in tasks
|
656
|
+
])
|
657
|
+
else:
|
658
|
+
# Run checks sequentially
|
659
|
+
results = []
|
660
|
+
for check_name in self.health_checks.keys():
|
661
|
+
result = await self.run_health_check(check_name)
|
662
|
+
results.append(result)
|
663
|
+
|
664
|
+
return results
|
665
|
+
|
666
|
+
async def get_health_summary(self) -> HealthSummary:
|
667
|
+
"""Get summary of all health checks."""
|
668
|
+
results = await self.run_all_health_checks()
|
669
|
+
|
670
|
+
healthy_count = sum(1 for r in results if r.status == HealthStatus.HEALTHY)
|
671
|
+
degraded_count = sum(1 for r in results if r.status == HealthStatus.DEGRADED)
|
672
|
+
unhealthy_count = sum(1 for r in results if r.status == HealthStatus.UNHEALTHY)
|
673
|
+
|
674
|
+
# Determine overall status
|
675
|
+
if unhealthy_count > 0:
|
676
|
+
overall_status = HealthStatus.UNHEALTHY
|
677
|
+
elif degraded_count > 0:
|
678
|
+
overall_status = HealthStatus.DEGRADED
|
679
|
+
elif healthy_count > 0:
|
680
|
+
overall_status = HealthStatus.HEALTHY
|
681
|
+
else:
|
682
|
+
overall_status = HealthStatus.UNKNOWN
|
683
|
+
|
684
|
+
return HealthSummary(
|
685
|
+
total_checks=len(results),
|
686
|
+
healthy_checks=healthy_count,
|
687
|
+
degraded_checks=degraded_count,
|
688
|
+
unhealthy_checks=unhealthy_count,
|
689
|
+
overall_status=overall_status,
|
690
|
+
details=results
|
691
|
+
)
|
692
|
+
|
693
|
+
def add_status_change_callback(self, callback: Callable):
|
694
|
+
"""Add callback for status changes."""
|
695
|
+
self.status_change_callbacks.append(callback)
|
696
|
+
|
697
|
+
def get_health_history(self, check_name: str, limit: int = None) -> List[HealthCheckResult]:
|
698
|
+
"""Get health check history for a specific check."""
|
699
|
+
history = self.history.get(check_name, [])
|
700
|
+
if limit:
|
701
|
+
return history[-limit:]
|
702
|
+
return history
|
703
|
+
|
704
|
+
async def _check_status_change(self, check_name: str, result: HealthCheckResult):
|
705
|
+
"""Check if status has changed and notify callbacks."""
|
706
|
+
if check_name in self.last_results:
|
707
|
+
previous = self.last_results[check_name]
|
708
|
+
if previous.status != result.status:
|
709
|
+
# Status changed, notify callbacks
|
710
|
+
for callback in self.status_change_callbacks:
|
711
|
+
try:
|
712
|
+
await callback(check_name, result)
|
713
|
+
except Exception as e:
|
714
|
+
logger.error(f"Error in status change callback: {e}")
|
715
|
+
|
716
|
+
async def shutdown(self):
|
717
|
+
"""Shutdown the health check manager."""
|
718
|
+
self._running = False
|
719
|
+
# Any cleanup logic here
|
720
|
+
|
721
|
+
|
722
|
+
# Add convenience functions for registering health checks
|
723
|
+
async def register_database_health_check(name: str, database_node, interval: float = 30.0):
|
724
|
+
"""Register a database health check with global manager."""
|
725
|
+
health_check = DatabaseHealthCheck(name, database_node)
|
726
|
+
manager = get_health_manager()
|
727
|
+
if hasattr(manager, 'register_health_check'):
|
728
|
+
manager.register_health_check(health_check, interval)
|
729
|
+
else:
|
730
|
+
manager.register_check(name, health_check)
|
731
|
+
|
732
|
+
|
733
|
+
async def register_memory_health_check(name: str, warning_threshold: float = 80.0,
|
734
|
+
critical_threshold: float = 95.0, interval: float = 30.0):
|
735
|
+
"""Register a memory health check with global manager."""
|
736
|
+
# This would need MemoryHealthCheck implementation
|
737
|
+
pass
|
738
|
+
|
739
|
+
|
740
|
+
async def register_custom_health_check(name: str, check_func: Callable,
|
741
|
+
interval: float = 30.0, timeout: float = 10.0):
|
742
|
+
"""Register a custom health check with global manager."""
|
743
|
+
# This would need CustomHealthCheck implementation
|
744
|
+
pass
|
745
|
+
|
746
|
+
|
747
|
+
def get_health_manager():
|
748
|
+
"""Get the global health manager instance."""
|
749
|
+
# Return a default HealthMonitor for now
|
750
|
+
return HealthMonitor()
|
kailash/edge/compliance.py
CHANGED
@@ -785,6 +785,39 @@ class ComplianceRouter:
|
|
785
785
|
"""Get recent compliance decisions from audit log."""
|
786
786
|
return self.audit_log[-limit:]
|
787
787
|
|
788
|
+
def is_compliant_location(
|
789
|
+
self,
|
790
|
+
location: "EdgeLocation",
|
791
|
+
data_class: DataClassification,
|
792
|
+
required_zones: List[str],
|
793
|
+
) -> bool:
|
794
|
+
"""Check if a location is compliant for given data class and zones.
|
795
|
+
|
796
|
+
Args:
|
797
|
+
location: Edge location to check
|
798
|
+
data_class: Classification of the data
|
799
|
+
required_zones: Required compliance zones
|
800
|
+
|
801
|
+
Returns:
|
802
|
+
True if location is compliant
|
803
|
+
"""
|
804
|
+
# Avoid circular import
|
805
|
+
from kailash.edge.location import EdgeRegion
|
806
|
+
|
807
|
+
# Check if location has all required compliance zones
|
808
|
+
location_zones = [z.value for z in location.compliance_zones]
|
809
|
+
|
810
|
+
# For GDPR compliance, PII/EU_PERSONAL data must be in EU regions or GDPR-compliant zones
|
811
|
+
if "gdpr" in required_zones and data_class in [
|
812
|
+
DataClassification.PII,
|
813
|
+
DataClassification.EU_PERSONAL,
|
814
|
+
]:
|
815
|
+
# Check if location has GDPR compliance zone
|
816
|
+
return "gdpr" in location_zones
|
817
|
+
|
818
|
+
# For other cases, check if location has the required zones
|
819
|
+
return all(zone in location_zones for zone in required_zones)
|
820
|
+
|
788
821
|
def get_compliance_summary(self) -> Dict[str, Any]:
|
789
822
|
"""Get summary of compliance decisions and performance."""
|
790
823
|
if not self.audit_log:
|