kailash 0.4.2__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/database/repositories.py +3 -1
- 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/audit_log.py +364 -6
- 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 +1209 -681
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- 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 +12 -25
- 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/utils/resource_manager.py +420 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +111 -41
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.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
|
+
)
|