kailash 0.5.0__py3-none-any.whl → 0.6.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control/__init__.py +1 -1
  3. kailash/client/__init__.py +12 -0
  4. kailash/client/enhanced_client.py +306 -0
  5. kailash/core/actors/__init__.py +16 -0
  6. kailash/core/actors/adaptive_pool_controller.py +630 -0
  7. kailash/core/actors/connection_actor.py +566 -0
  8. kailash/core/actors/supervisor.py +364 -0
  9. kailash/core/ml/__init__.py +1 -0
  10. kailash/core/ml/query_patterns.py +544 -0
  11. kailash/core/monitoring/__init__.py +19 -0
  12. kailash/core/monitoring/connection_metrics.py +488 -0
  13. kailash/core/optimization/__init__.py +1 -0
  14. kailash/core/resilience/__init__.py +17 -0
  15. kailash/core/resilience/circuit_breaker.py +382 -0
  16. kailash/edge/__init__.py +16 -0
  17. kailash/edge/compliance.py +834 -0
  18. kailash/edge/discovery.py +659 -0
  19. kailash/edge/location.py +582 -0
  20. kailash/gateway/__init__.py +33 -0
  21. kailash/gateway/api.py +289 -0
  22. kailash/gateway/enhanced_gateway.py +357 -0
  23. kailash/gateway/resource_resolver.py +217 -0
  24. kailash/gateway/security.py +227 -0
  25. kailash/middleware/auth/access_control.py +6 -6
  26. kailash/middleware/auth/models.py +2 -2
  27. kailash/middleware/communication/ai_chat.py +7 -7
  28. kailash/middleware/communication/api_gateway.py +5 -15
  29. kailash/middleware/database/base_models.py +1 -7
  30. kailash/middleware/gateway/__init__.py +22 -0
  31. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  32. kailash/middleware/gateway/deduplicator.py +382 -0
  33. kailash/middleware/gateway/durable_gateway.py +417 -0
  34. kailash/middleware/gateway/durable_request.py +498 -0
  35. kailash/middleware/gateway/event_store.py +499 -0
  36. kailash/middleware/mcp/enhanced_server.py +2 -2
  37. kailash/nodes/admin/permission_check.py +817 -33
  38. kailash/nodes/admin/role_management.py +1242 -108
  39. kailash/nodes/admin/schema_manager.py +438 -0
  40. kailash/nodes/admin/user_management.py +1124 -1582
  41. kailash/nodes/code/__init__.py +8 -1
  42. kailash/nodes/code/async_python.py +1035 -0
  43. kailash/nodes/code/python.py +1 -0
  44. kailash/nodes/data/async_sql.py +9 -3
  45. kailash/nodes/data/query_pipeline.py +641 -0
  46. kailash/nodes/data/query_router.py +895 -0
  47. kailash/nodes/data/sql.py +20 -11
  48. kailash/nodes/data/workflow_connection_pool.py +1071 -0
  49. kailash/nodes/monitoring/__init__.py +3 -5
  50. kailash/nodes/monitoring/connection_dashboard.py +822 -0
  51. kailash/nodes/rag/__init__.py +2 -7
  52. kailash/resources/__init__.py +40 -0
  53. kailash/resources/factory.py +533 -0
  54. kailash/resources/health.py +319 -0
  55. kailash/resources/reference.py +288 -0
  56. kailash/resources/registry.py +392 -0
  57. kailash/runtime/async_local.py +711 -302
  58. kailash/testing/__init__.py +34 -0
  59. kailash/testing/async_test_case.py +353 -0
  60. kailash/testing/async_utils.py +345 -0
  61. kailash/testing/fixtures.py +458 -0
  62. kailash/testing/mock_registry.py +495 -0
  63. kailash/workflow/__init__.py +8 -0
  64. kailash/workflow/async_builder.py +621 -0
  65. kailash/workflow/async_patterns.py +766 -0
  66. kailash/workflow/cyclic_runner.py +107 -16
  67. kailash/workflow/graph.py +7 -2
  68. kailash/workflow/resilience.py +11 -1
  69. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/METADATA +19 -4
  70. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/RECORD +74 -28
  71. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/WHEEL +0 -0
  72. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/entry_points.txt +0 -0
  73. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/licenses/LICENSE +0 -0
  74. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,392 @@
1
+ """
2
+ Resource Registry - Central management for shared resources in Kailash workflows.
3
+
4
+ This registry solves the JSON serialization problem by allowing workflows to
5
+ reference resources by name rather than passing the actual objects.
6
+ """
7
+
8
+ import asyncio
9
+ import logging
10
+ import time
11
+ import weakref
12
+ from datetime import datetime, timedelta
13
+ from typing import Any, Callable, Dict, Optional, Set
14
+
15
+ from .factory import ResourceFactory
16
+ from .health import HealthCheck, HealthStatus
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class ResourceNotFoundError(Exception):
22
+ """Raised when a requested resource is not found in the registry."""
23
+
24
+ pass
25
+
26
+
27
+ class ResourceRegistry:
28
+ """
29
+ Central registry for shared resources in Kailash workflows.
30
+
31
+ This registry provides:
32
+ - Lazy initialization of resources
33
+ - Health checking and automatic recovery
34
+ - Resource lifecycle management
35
+ - Thread-safe access with async locks
36
+ - Metrics collection for monitoring
37
+
38
+ Example:
39
+ ```python
40
+ # Create registry
41
+ registry = ResourceRegistry()
42
+
43
+ # Register a database factory
44
+ registry.register_factory(
45
+ 'main_db',
46
+ DatabasePoolFactory(host='localhost', database='myapp'),
47
+ health_check=lambda pool: pool.ping()
48
+ )
49
+
50
+ # Get resource in workflow
51
+ db = await registry.get_resource('main_db')
52
+ ```
53
+ """
54
+
55
+ def __init__(self, enable_metrics: bool = True):
56
+ """
57
+ Initialize the resource registry.
58
+
59
+ Args:
60
+ enable_metrics: Whether to collect usage metrics
61
+ """
62
+ self._resources: Dict[str, Any] = {}
63
+ self._factories: Dict[str, ResourceFactory] = {}
64
+ self._locks: Dict[str, asyncio.Lock] = {}
65
+ self._health_checks: Dict[str, HealthCheck] = {}
66
+ self._cleanup_handlers: Dict[str, Callable] = {}
67
+ self._resource_metadata: Dict[str, Dict[str, Any]] = {}
68
+ self._global_lock = asyncio.Lock()
69
+ self._enable_metrics = enable_metrics
70
+
71
+ # Metrics tracking
72
+ self._metrics = {
73
+ "resource_creations": {},
74
+ "resource_accesses": {},
75
+ "health_check_failures": {},
76
+ "resource_recreations": {},
77
+ }
78
+
79
+ # Circuit breaker state
80
+ self._circuit_breakers: Dict[str, Dict[str, Any]] = {}
81
+
82
+ # Weak references for garbage collection
83
+ self._weak_refs: Dict[str, weakref.ref] = {}
84
+
85
+ def register_factory(
86
+ self,
87
+ name: str,
88
+ factory: ResourceFactory,
89
+ health_check: Optional[HealthCheck] = None,
90
+ cleanup_handler: Optional[Callable] = None,
91
+ metadata: Optional[Dict[str, Any]] = None,
92
+ ) -> None:
93
+ """
94
+ Register a resource factory with optional health check.
95
+
96
+ Args:
97
+ name: Unique name for the resource
98
+ factory: Factory that creates the resource
99
+ health_check: Optional health check callable
100
+ cleanup_handler: Optional cleanup callable
101
+ metadata: Optional metadata about the resource
102
+ """
103
+ if name in self._factories:
104
+ logger.warning(f"Overwriting existing factory for resource: {name}")
105
+
106
+ self._factories[name] = factory
107
+ self._locks[name] = asyncio.Lock()
108
+
109
+ if health_check:
110
+ self._health_checks[name] = health_check
111
+
112
+ if cleanup_handler:
113
+ self._cleanup_handlers[name] = cleanup_handler
114
+
115
+ if metadata:
116
+ self._resource_metadata[name] = metadata
117
+
118
+ # Initialize circuit breaker
119
+ self._circuit_breakers[name] = {
120
+ "failures": 0,
121
+ "last_failure": None,
122
+ "state": "closed", # closed, open, half-open
123
+ "threshold": (
124
+ metadata.get("circuit_breaker_threshold", 5) if metadata else 5
125
+ ),
126
+ }
127
+
128
+ logger.info(f"Registered factory for resource: {name}")
129
+
130
+ async def get_resource(self, name: str) -> Any:
131
+ """
132
+ Get or create a resource by name.
133
+
134
+ This method:
135
+ 1. Checks if resource exists and is healthy
136
+ 2. Creates the resource if needed
137
+ 3. Implements circuit breaker pattern
138
+ 4. Tracks metrics
139
+
140
+ Args:
141
+ name: Name of the resource
142
+
143
+ Returns:
144
+ The requested resource
145
+
146
+ Raises:
147
+ ResourceNotFoundError: If no factory is registered
148
+ Exception: If resource creation fails
149
+ """
150
+ # Check circuit breaker
151
+ if self._is_circuit_open(name):
152
+ raise ResourceNotFoundError(f"Circuit breaker open for resource: {name}")
153
+
154
+ async with self._locks.get(name, self._global_lock):
155
+ try:
156
+ # Track access
157
+ if self._enable_metrics:
158
+ self._track_access(name)
159
+
160
+ # Check if resource exists and is healthy
161
+ if name in self._resources:
162
+ if await self._is_healthy(name):
163
+ self._reset_circuit_breaker(name)
164
+ return self._resources[name]
165
+ else:
166
+ # Recreate unhealthy resource
167
+ logger.warning(f"Resource {name} is unhealthy, recreating")
168
+ await self._cleanup_resource(name)
169
+ if self._enable_metrics:
170
+ self._metrics["resource_recreations"][name] = (
171
+ self._metrics["resource_recreations"].get(name, 0) + 1
172
+ )
173
+
174
+ # Create resource
175
+ if name not in self._factories:
176
+ raise ResourceNotFoundError(
177
+ f"No factory registered for resource: {name}"
178
+ )
179
+
180
+ logger.info(f"Creating resource: {name}")
181
+ start_time = time.time()
182
+
183
+ resource = await self._factories[name].create()
184
+
185
+ # Store resource
186
+ self._resources[name] = resource
187
+
188
+ # Store weak reference if possible
189
+ try:
190
+ self._weak_refs[name] = weakref.ref(
191
+ resource, lambda ref: self._on_resource_collected(name)
192
+ )
193
+ except TypeError:
194
+ # Some objects don't support weak references
195
+ pass
196
+
197
+ # Track creation time
198
+ if self._enable_metrics:
199
+ creation_time = time.time() - start_time
200
+ self._metrics["resource_creations"][name] = (
201
+ self._metrics["resource_creations"].get(name, 0) + 1
202
+ )
203
+ logger.info(f"Created resource {name} in {creation_time:.2f}s")
204
+
205
+ self._reset_circuit_breaker(name)
206
+ return resource
207
+
208
+ except Exception as e:
209
+ self._record_circuit_breaker_failure(name)
210
+ logger.error(f"Failed to get resource {name}: {e}")
211
+ raise
212
+
213
+ async def _is_healthy(self, name: str) -> bool:
214
+ """Check if a resource is healthy."""
215
+ if name not in self._health_checks:
216
+ return True # No health check = assume healthy
217
+
218
+ try:
219
+ health_check = self._health_checks[name]
220
+ resource = self._resources[name]
221
+
222
+ # Support both sync and async health checks
223
+ if asyncio.iscoroutinefunction(health_check):
224
+ result = await health_check(resource)
225
+ else:
226
+ result = await asyncio.get_event_loop().run_in_executor(
227
+ None, health_check, resource
228
+ )
229
+
230
+ # Handle different return types
231
+ if isinstance(result, bool):
232
+ return result
233
+ elif isinstance(result, HealthStatus):
234
+ return result.is_healthy
235
+ else:
236
+ return bool(result)
237
+
238
+ except Exception as e:
239
+ logger.error(f"Health check failed for {name}: {e}")
240
+ if self._enable_metrics:
241
+ self._metrics["health_check_failures"][name] = (
242
+ self._metrics["health_check_failures"].get(name, 0) + 1
243
+ )
244
+ return False
245
+
246
+ async def _cleanup_resource(self, name: str) -> None:
247
+ """Clean up a resource."""
248
+ if name not in self._resources:
249
+ return
250
+
251
+ try:
252
+ resource = self._resources.pop(name)
253
+
254
+ # Remove weak reference
255
+ if name in self._weak_refs:
256
+ del self._weak_refs[name]
257
+
258
+ # Call cleanup handler if available
259
+ if name in self._cleanup_handlers:
260
+ cleanup = self._cleanup_handlers[name]
261
+ if asyncio.iscoroutinefunction(cleanup):
262
+ await cleanup(resource)
263
+ else:
264
+ await asyncio.get_event_loop().run_in_executor(
265
+ None, cleanup, resource
266
+ )
267
+
268
+ # Try generic cleanup methods
269
+ elif hasattr(resource, "close"):
270
+ if asyncio.iscoroutinefunction(resource.close):
271
+ await resource.close()
272
+ else:
273
+ resource.close()
274
+ elif hasattr(resource, "cleanup"):
275
+ if asyncio.iscoroutinefunction(resource.cleanup):
276
+ await resource.cleanup()
277
+ else:
278
+ resource.cleanup()
279
+ elif hasattr(resource, "disconnect"):
280
+ if asyncio.iscoroutinefunction(resource.disconnect):
281
+ await resource.disconnect()
282
+ else:
283
+ resource.disconnect()
284
+
285
+ logger.info(f"Cleaned up resource: {name}")
286
+
287
+ except Exception as e:
288
+ logger.error(f"Error cleaning up resource {name}: {e}")
289
+
290
+ async def cleanup(self) -> None:
291
+ """Clean up all resources."""
292
+ logger.info("Cleaning up all resources")
293
+
294
+ # Clean up in parallel
295
+ tasks = []
296
+ for name in list(self._resources.keys()):
297
+ tasks.append(self._cleanup_resource(name))
298
+
299
+ if tasks:
300
+ await asyncio.gather(*tasks, return_exceptions=True)
301
+
302
+ # Clear all state
303
+ self._resources.clear()
304
+ self._weak_refs.clear()
305
+ self._circuit_breakers.clear()
306
+
307
+ def has_resource(self, name: str) -> bool:
308
+ """Check if a resource exists in the registry."""
309
+ return name in self._resources
310
+
311
+ def has_factory(self, name: str) -> bool:
312
+ """Check if a factory is registered."""
313
+ return name in self._factories
314
+
315
+ def list_resources(self) -> Set[str]:
316
+ """List all registered resource names."""
317
+ return set(self._factories.keys())
318
+
319
+ def get_metrics(self) -> Dict[str, Any]:
320
+ """Get resource usage metrics."""
321
+ if not self._enable_metrics:
322
+ return {}
323
+
324
+ return {
325
+ "resources": {
326
+ name: {
327
+ "created": self._metrics["resource_creations"].get(name, 0),
328
+ "accessed": self._metrics["resource_accesses"].get(name, 0),
329
+ "health_failures": self._metrics["health_check_failures"].get(
330
+ name, 0
331
+ ),
332
+ "recreations": self._metrics["resource_recreations"].get(name, 0),
333
+ "circuit_breaker": self._circuit_breakers.get(name, {}),
334
+ }
335
+ for name in self._factories.keys()
336
+ }
337
+ }
338
+
339
+ # Circuit breaker methods
340
+ def _is_circuit_open(self, name: str) -> bool:
341
+ """Check if circuit breaker is open."""
342
+ if name not in self._circuit_breakers:
343
+ return False
344
+
345
+ breaker = self._circuit_breakers[name]
346
+
347
+ if breaker["state"] == "open":
348
+ # Check if we should try half-open
349
+ if breaker["last_failure"]:
350
+ time_since_failure = datetime.now() - breaker["last_failure"]
351
+ if time_since_failure > timedelta(seconds=30):
352
+ breaker["state"] = "half-open"
353
+ return False
354
+ return True
355
+
356
+ return False
357
+
358
+ def _record_circuit_breaker_failure(self, name: str) -> None:
359
+ """Record a failure for circuit breaker."""
360
+ if name not in self._circuit_breakers:
361
+ return
362
+
363
+ breaker = self._circuit_breakers[name]
364
+ breaker["failures"] += 1
365
+ breaker["last_failure"] = datetime.now()
366
+
367
+ if breaker["failures"] >= breaker["threshold"]:
368
+ breaker["state"] = "open"
369
+ logger.error(f"Circuit breaker opened for resource: {name}")
370
+
371
+ def _reset_circuit_breaker(self, name: str) -> None:
372
+ """Reset circuit breaker on success."""
373
+ if name not in self._circuit_breakers:
374
+ return
375
+
376
+ breaker = self._circuit_breakers[name]
377
+ breaker["failures"] = 0
378
+ breaker["last_failure"] = None
379
+ breaker["state"] = "closed"
380
+
381
+ # Metrics tracking
382
+ def _track_access(self, name: str) -> None:
383
+ """Track resource access."""
384
+ self._metrics["resource_accesses"][name] = (
385
+ self._metrics["resource_accesses"].get(name, 0) + 1
386
+ )
387
+
388
+ def _on_resource_collected(self, name: str) -> None:
389
+ """Callback when a resource is garbage collected."""
390
+ logger.debug(f"Resource {name} was garbage collected")
391
+ # Remove from resources dict if still there
392
+ self._resources.pop(name, None)