kailash 0.5.0__py3-none-any.whl → 0.6.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 +1 -1
- kailash/client/__init__.py +12 -0
- kailash/client/enhanced_client.py +306 -0
- kailash/core/actors/__init__.py +16 -0
- kailash/core/actors/connection_actor.py +566 -0
- kailash/core/actors/supervisor.py +364 -0
- kailash/edge/__init__.py +16 -0
- kailash/edge/compliance.py +834 -0
- kailash/edge/discovery.py +659 -0
- kailash/edge/location.py +582 -0
- kailash/gateway/__init__.py +33 -0
- kailash/gateway/api.py +289 -0
- kailash/gateway/enhanced_gateway.py +357 -0
- kailash/gateway/resource_resolver.py +217 -0
- kailash/gateway/security.py +227 -0
- kailash/middleware/auth/models.py +2 -2
- kailash/middleware/database/base_models.py +1 -7
- kailash/middleware/gateway/__init__.py +22 -0
- kailash/middleware/gateway/checkpoint_manager.py +398 -0
- kailash/middleware/gateway/deduplicator.py +382 -0
- kailash/middleware/gateway/durable_gateway.py +417 -0
- kailash/middleware/gateway/durable_request.py +498 -0
- kailash/middleware/gateway/event_store.py +459 -0
- kailash/nodes/admin/permission_check.py +817 -33
- kailash/nodes/admin/role_management.py +1242 -108
- kailash/nodes/admin/schema_manager.py +438 -0
- kailash/nodes/admin/user_management.py +1124 -1582
- kailash/nodes/code/__init__.py +8 -1
- kailash/nodes/code/async_python.py +1035 -0
- kailash/nodes/code/python.py +1 -0
- kailash/nodes/data/async_sql.py +9 -3
- kailash/nodes/data/sql.py +20 -11
- kailash/nodes/data/workflow_connection_pool.py +643 -0
- kailash/nodes/rag/__init__.py +1 -4
- kailash/resources/__init__.py +40 -0
- kailash/resources/factory.py +533 -0
- kailash/resources/health.py +319 -0
- kailash/resources/reference.py +288 -0
- kailash/resources/registry.py +392 -0
- kailash/runtime/async_local.py +711 -302
- kailash/testing/__init__.py +34 -0
- kailash/testing/async_test_case.py +353 -0
- kailash/testing/async_utils.py +345 -0
- kailash/testing/fixtures.py +458 -0
- kailash/testing/mock_registry.py +495 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/cyclic_runner.py +107 -16
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.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)
|