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,345 @@
1
+ """Async testing utilities."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import time
6
+ from contextlib import asynccontextmanager
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Callable,
11
+ Coroutine,
12
+ Dict,
13
+ List,
14
+ Optional,
15
+ Type,
16
+ TypeVar,
17
+ Union,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from kailash.runtime.async_local import AsyncLocalRuntime
22
+ from kailash.workflow import Workflow
23
+
24
+ T = TypeVar("T")
25
+
26
+
27
+ class AsyncTestUtils:
28
+ """Utilities for async testing."""
29
+
30
+ @staticmethod
31
+ async def wait_for_condition(
32
+ condition: Callable[[], Union[bool, Coroutine[Any, Any, bool]]],
33
+ timeout: float = 5.0,
34
+ interval: float = 0.1,
35
+ message: str = "Condition not met",
36
+ ):
37
+ """Wait for a condition to become true."""
38
+ start = time.time()
39
+ while time.time() - start < timeout:
40
+ # Evaluate condition
41
+ if asyncio.iscoroutinefunction(condition):
42
+ result = await condition()
43
+ else:
44
+ result = condition()
45
+
46
+ if result:
47
+ return
48
+
49
+ await asyncio.sleep(interval)
50
+
51
+ raise TimeoutError(f"{message} after {timeout}s")
52
+
53
+ @staticmethod
54
+ async def assert_completes_within(
55
+ coro: Coroutine, seconds: float, message: str = None
56
+ ) -> T:
57
+ """Assert that a coroutine completes within time limit."""
58
+ try:
59
+ return await asyncio.wait_for(coro, timeout=seconds)
60
+ except asyncio.TimeoutError:
61
+ msg = message or f"Coroutine did not complete within {seconds}s"
62
+ raise AssertionError(msg)
63
+
64
+ @staticmethod
65
+ async def assert_raises_async(
66
+ exception_type: Type[Exception],
67
+ coro: Union[Coroutine, Callable],
68
+ *args,
69
+ **kwargs,
70
+ ):
71
+ """Assert that a coroutine raises specific exception."""
72
+ try:
73
+ if asyncio.iscoroutine(coro):
74
+ await coro
75
+ else:
76
+ await coro(*args, **kwargs)
77
+
78
+ raise AssertionError(
79
+ f"Expected {exception_type.__name__} but no exception was raised"
80
+ )
81
+ except exception_type as e:
82
+ return e # Return exception for further assertions
83
+ except Exception as e:
84
+ raise AssertionError(
85
+ f"Expected {exception_type.__name__} but got "
86
+ f"{type(e).__name__}: {e}"
87
+ )
88
+
89
+ @staticmethod
90
+ @asynccontextmanager
91
+ async def assert_duration(min_seconds: float = None, max_seconds: float = None):
92
+ """Context manager to assert execution duration."""
93
+ start = asyncio.get_event_loop().time()
94
+ yield
95
+ duration = asyncio.get_event_loop().time() - start
96
+
97
+ if min_seconds is not None and duration < min_seconds:
98
+ raise AssertionError(
99
+ f"Operation completed too quickly: {duration:.3f}s < {min_seconds}s"
100
+ )
101
+
102
+ if max_seconds is not None and duration > max_seconds:
103
+ raise AssertionError(
104
+ f"Operation took too long: {duration:.3f}s > {max_seconds}s"
105
+ )
106
+
107
+ @staticmethod
108
+ async def run_concurrent(
109
+ *coroutines: Coroutine, return_exceptions: bool = False
110
+ ) -> List[Any]:
111
+ """Run multiple coroutines concurrently."""
112
+ return await asyncio.gather(*coroutines, return_exceptions=return_exceptions)
113
+
114
+ @staticmethod
115
+ async def run_sequential(*coroutines: Coroutine) -> List[Any]:
116
+ """Run multiple coroutines sequentially."""
117
+ results = []
118
+ for coro in coroutines:
119
+ result = await coro
120
+ results.append(result)
121
+ return results
122
+
123
+ @staticmethod
124
+ def async_retry(
125
+ max_attempts: int = 3,
126
+ delay: float = 0.1,
127
+ backoff: float = 2.0,
128
+ exceptions: tuple = (Exception,),
129
+ ):
130
+ """Decorator to retry async functions."""
131
+
132
+ def decorator(func):
133
+ @functools.wraps(func)
134
+ async def wrapper(*args, **kwargs):
135
+ last_exception = None
136
+ current_delay = delay
137
+
138
+ for attempt in range(max_attempts):
139
+ try:
140
+ return await func(*args, **kwargs)
141
+ except exceptions as e:
142
+ last_exception = e
143
+ if attempt < max_attempts - 1:
144
+ await asyncio.sleep(current_delay)
145
+ current_delay *= backoff
146
+
147
+ raise last_exception
148
+
149
+ return wrapper
150
+
151
+ return decorator
152
+
153
+
154
+ class AsyncAssertions:
155
+ """Async-aware assertions for testing."""
156
+
157
+ @staticmethod
158
+ async def assert_eventually_equals(
159
+ getter: Callable[[], Union[Any, Coroutine[Any, Any, Any]]],
160
+ expected: Any,
161
+ timeout: float = 5.0,
162
+ interval: float = 0.1,
163
+ message: str = None,
164
+ ):
165
+ """Assert that a value eventually equals expected."""
166
+
167
+ async def condition():
168
+ if asyncio.iscoroutinefunction(getter):
169
+ value = await getter()
170
+ else:
171
+ value = getter()
172
+ return value == expected
173
+
174
+ await AsyncTestUtils.wait_for_condition(
175
+ condition,
176
+ timeout=timeout,
177
+ interval=interval,
178
+ message=message or f"Value did not equal {expected}",
179
+ )
180
+
181
+ @staticmethod
182
+ async def assert_eventually_true(
183
+ condition: Callable[[], Union[bool, Coroutine[Any, Any, bool]]],
184
+ timeout: float = 5.0,
185
+ message: str = None,
186
+ ):
187
+ """Assert that a condition eventually becomes true."""
188
+ await AsyncTestUtils.wait_for_condition(
189
+ condition,
190
+ timeout=timeout,
191
+ message=message or "Condition did not become true",
192
+ )
193
+
194
+ @staticmethod
195
+ async def assert_converges(
196
+ getter: Callable[[], Union[float, Coroutine[Any, Any, float]]],
197
+ tolerance: float = 0.01,
198
+ timeout: float = 10.0,
199
+ samples: int = 5,
200
+ ):
201
+ """Assert that a value converges to a stable state."""
202
+ values = []
203
+ sample_interval = timeout / (samples + 1)
204
+
205
+ for _ in range(samples):
206
+ if asyncio.iscoroutinefunction(getter):
207
+ value = await getter()
208
+ else:
209
+ value = getter()
210
+ values.append(value)
211
+ await asyncio.sleep(sample_interval)
212
+
213
+ # Check if values converged
214
+ if len(values) < 2:
215
+ return
216
+
217
+ max_diff = max(abs(values[i] - values[i - 1]) for i in range(1, len(values)))
218
+ assert max_diff <= tolerance, (
219
+ f"Values did not converge within tolerance {tolerance}\n"
220
+ f"Values: {values}\n"
221
+ f"Max difference: {max_diff}"
222
+ )
223
+
224
+ @staticmethod
225
+ async def assert_workflow_succeeds(
226
+ workflow: "Workflow",
227
+ inputs: Dict[str, Any],
228
+ runtime: "AsyncLocalRuntime",
229
+ timeout: float = 30.0,
230
+ ) -> Dict[str, Any]:
231
+ """Assert workflow executes successfully."""
232
+ from ..runtime.async_local import AsyncLocalRuntime
233
+
234
+ result = await AsyncTestUtils.assert_completes_within(
235
+ runtime.execute_workflow_async(workflow, inputs),
236
+ timeout,
237
+ f"Workflow did not complete within {timeout}s",
238
+ )
239
+
240
+ if hasattr(result, "errors") and result.errors:
241
+ raise AssertionError(
242
+ "Workflow failed with errors:\n"
243
+ + "\n".join(f" {node}: {error}" for node, error in result.errors)
244
+ )
245
+
246
+ return result
247
+
248
+ @staticmethod
249
+ async def assert_concurrent_safe(
250
+ func: Callable, *args, concurrency: int = 10, **kwargs
251
+ ):
252
+ """Assert function is safe for concurrent execution."""
253
+ # Run function multiple times concurrently
254
+ tasks = []
255
+ for _ in range(concurrency):
256
+ if asyncio.iscoroutinefunction(func):
257
+ tasks.append(func(*args, **kwargs))
258
+ else:
259
+ tasks.append(
260
+ asyncio.get_event_loop().run_in_executor(
261
+ None, func, *args, **kwargs
262
+ )
263
+ )
264
+
265
+ # All should complete without error
266
+ results = await asyncio.gather(*tasks, return_exceptions=True)
267
+
268
+ # Check for exceptions
269
+ exceptions = [r for r in results if isinstance(r, Exception)]
270
+ assert (
271
+ not exceptions
272
+ ), f"Concurrent execution failed with {len(exceptions)} errors:\n" + "\n".join(
273
+ str(e) for e in exceptions
274
+ )
275
+
276
+ @staticmethod
277
+ async def assert_performance(
278
+ coro: Coroutine,
279
+ max_time: float = None,
280
+ min_throughput: float = None,
281
+ operations: int = 1,
282
+ ) -> Any:
283
+ """Assert performance requirements are met."""
284
+ start = asyncio.get_event_loop().time()
285
+ result = await coro
286
+ duration = asyncio.get_event_loop().time() - start
287
+
288
+ if max_time is not None:
289
+ assert (
290
+ duration <= max_time
291
+ ), f"Operation took {duration:.3f}s, exceeding max {max_time}s"
292
+
293
+ if min_throughput is not None:
294
+ throughput = operations / duration
295
+ assert (
296
+ throughput >= min_throughput
297
+ ), f"Throughput {throughput:.1f} ops/s below minimum {min_throughput} ops/s"
298
+
299
+ return result
300
+
301
+ @staticmethod
302
+ async def assert_memory_stable(
303
+ func: Callable,
304
+ *args,
305
+ iterations: int = 100,
306
+ growth_tolerance: float = 0.1,
307
+ **kwargs,
308
+ ):
309
+ """Assert that repeated execution doesn't leak memory."""
310
+ import gc
311
+ import os
312
+
313
+ import psutil
314
+
315
+ process = psutil.Process(os.getpid())
316
+
317
+ # Warm up and measure baseline
318
+ for _ in range(10):
319
+ if asyncio.iscoroutinefunction(func):
320
+ await func(*args, **kwargs)
321
+ else:
322
+ func(*args, **kwargs)
323
+
324
+ gc.collect()
325
+ await asyncio.sleep(0.1)
326
+ baseline_memory = process.memory_info().rss
327
+
328
+ # Run iterations
329
+ for _ in range(iterations):
330
+ if asyncio.iscoroutinefunction(func):
331
+ await func(*args, **kwargs)
332
+ else:
333
+ func(*args, **kwargs)
334
+
335
+ gc.collect()
336
+ await asyncio.sleep(0.1)
337
+ final_memory = process.memory_info().rss
338
+
339
+ # Check growth
340
+ growth = (final_memory - baseline_memory) / baseline_memory
341
+ assert growth <= growth_tolerance, (
342
+ f"Memory grew by {growth:.1%}, exceeding tolerance of {growth_tolerance:.1%}\n"
343
+ f"Baseline: {baseline_memory / 1024 / 1024:.1f}MB\n"
344
+ f"Final: {final_memory / 1024 / 1024:.1f}MB"
345
+ )