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.
Files changed (57) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/client/__init__.py +12 -0
  3. kailash/client/enhanced_client.py +306 -0
  4. kailash/core/actors/__init__.py +16 -0
  5. kailash/core/actors/connection_actor.py +566 -0
  6. kailash/core/actors/supervisor.py +364 -0
  7. kailash/edge/__init__.py +16 -0
  8. kailash/edge/compliance.py +834 -0
  9. kailash/edge/discovery.py +659 -0
  10. kailash/edge/location.py +582 -0
  11. kailash/gateway/__init__.py +33 -0
  12. kailash/gateway/api.py +289 -0
  13. kailash/gateway/enhanced_gateway.py +357 -0
  14. kailash/gateway/resource_resolver.py +217 -0
  15. kailash/gateway/security.py +227 -0
  16. kailash/middleware/auth/models.py +2 -2
  17. kailash/middleware/database/base_models.py +1 -7
  18. kailash/middleware/gateway/__init__.py +22 -0
  19. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  20. kailash/middleware/gateway/deduplicator.py +382 -0
  21. kailash/middleware/gateway/durable_gateway.py +417 -0
  22. kailash/middleware/gateway/durable_request.py +498 -0
  23. kailash/middleware/gateway/event_store.py +459 -0
  24. kailash/nodes/admin/permission_check.py +817 -33
  25. kailash/nodes/admin/role_management.py +1242 -108
  26. kailash/nodes/admin/schema_manager.py +438 -0
  27. kailash/nodes/admin/user_management.py +1124 -1582
  28. kailash/nodes/code/__init__.py +8 -1
  29. kailash/nodes/code/async_python.py +1035 -0
  30. kailash/nodes/code/python.py +1 -0
  31. kailash/nodes/data/async_sql.py +9 -3
  32. kailash/nodes/data/sql.py +20 -11
  33. kailash/nodes/data/workflow_connection_pool.py +643 -0
  34. kailash/nodes/rag/__init__.py +1 -4
  35. kailash/resources/__init__.py +40 -0
  36. kailash/resources/factory.py +533 -0
  37. kailash/resources/health.py +319 -0
  38. kailash/resources/reference.py +288 -0
  39. kailash/resources/registry.py +392 -0
  40. kailash/runtime/async_local.py +711 -302
  41. kailash/testing/__init__.py +34 -0
  42. kailash/testing/async_test_case.py +353 -0
  43. kailash/testing/async_utils.py +345 -0
  44. kailash/testing/fixtures.py +458 -0
  45. kailash/testing/mock_registry.py +495 -0
  46. kailash/workflow/__init__.py +8 -0
  47. kailash/workflow/async_builder.py +621 -0
  48. kailash/workflow/async_patterns.py +766 -0
  49. kailash/workflow/cyclic_runner.py +107 -16
  50. kailash/workflow/graph.py +7 -2
  51. kailash/workflow/resilience.py +11 -1
  52. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
  53. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
  54. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  55. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  56. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  57. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,34 @@
1
+ """Kailash Async Testing Framework.
2
+
3
+ Comprehensive testing utilities for async workflows.
4
+ """
5
+
6
+ from .async_test_case import AsyncWorkflowTestCase, WorkflowTestResult
7
+ from .async_utils import AsyncAssertions, AsyncTestUtils
8
+ from .fixtures import (
9
+ AsyncWorkflowFixtures,
10
+ DatabaseFixture,
11
+ MockCache,
12
+ MockHttpClient,
13
+ TestHttpServer,
14
+ )
15
+ from .mock_registry import CallRecord, MockResource, MockResourceRegistry
16
+
17
+ __all__ = [
18
+ # Core test case
19
+ "AsyncWorkflowTestCase",
20
+ "WorkflowTestResult",
21
+ # Mock system
22
+ "MockResourceRegistry",
23
+ "CallRecord",
24
+ "MockResource",
25
+ # Async utilities
26
+ "AsyncTestUtils",
27
+ "AsyncAssertions",
28
+ # Fixtures
29
+ "AsyncWorkflowFixtures",
30
+ "MockHttpClient",
31
+ "MockCache",
32
+ "DatabaseFixture",
33
+ "TestHttpServer",
34
+ ]
@@ -0,0 +1,353 @@
1
+ """Base class for async workflow testing."""
2
+
3
+ import asyncio
4
+ import functools
5
+ import logging
6
+ from contextlib import asynccontextmanager
7
+ from dataclasses import dataclass, field
8
+ from datetime import datetime, timezone
9
+ from typing import Any, Callable, Dict, List, Optional, TypeVar, Union
10
+
11
+ from ..resources.registry import ResourceFactory, ResourceRegistry
12
+ from ..runtime.async_local import AsyncLocalRuntime, ExecutionContext
13
+ from ..workflow.graph import Workflow
14
+ from .mock_registry import MockResourceRegistry
15
+
16
+ T = TypeVar("T")
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @dataclass
21
+ class WorkflowTestResult:
22
+ """Result from test workflow execution."""
23
+
24
+ status: str
25
+ outputs: Dict[str, Any]
26
+ errors: List[tuple[str, Exception]] = field(default_factory=list)
27
+ error: Optional[str] = None
28
+ execution_time: float = 0.0
29
+ node_timings: Dict[str, float] = field(default_factory=dict)
30
+ logs: List[str] = field(default_factory=list)
31
+ stdout: str = ""
32
+ stderr: str = ""
33
+
34
+ def get_output(self, node_id: str, key: Optional[str] = None) -> Any:
35
+ """Get output from specific node."""
36
+ output = self.outputs.get(node_id)
37
+
38
+ if key and isinstance(output, dict):
39
+ # Handle nested keys
40
+ keys = key.split(".")
41
+ for k in keys:
42
+ if isinstance(output, dict):
43
+ output = output.get(k)
44
+ else:
45
+ return None
46
+
47
+ return output
48
+
49
+ def get_logs(self) -> str:
50
+ """Get formatted logs."""
51
+ return "\n".join(self.logs)
52
+
53
+ def print_summary(self):
54
+ """Print execution summary."""
55
+ print(f"Status: {self.status}")
56
+ print(f"Execution time: {self.execution_time:.2f}s")
57
+
58
+ if self.errors:
59
+ print(f"Errors: {len(self.errors)}")
60
+ for node_id, error in self.errors:
61
+ print(f" - {node_id}: {error}")
62
+
63
+ print(f"Nodes executed: {len(self.outputs)}")
64
+ for node_id, timing in self.node_timings.items():
65
+ print(f" - {node_id}: {timing:.3f}s")
66
+
67
+
68
+ class AsyncWorkflowTestCase:
69
+ """Base class for async workflow testing."""
70
+
71
+ def __init__(self, test_name: str = None):
72
+ self.test_name = test_name or self.__class__.__name__
73
+ self.resource_registry = ResourceRegistry()
74
+ self.mock_registry = MockResourceRegistry()
75
+ self._cleanup_tasks: List[Callable] = []
76
+ self._test_resources: Dict[str, Any] = {}
77
+ self._assertions_made = 0
78
+ self._start_time: Optional[datetime] = None
79
+
80
+ async def setUp(self):
81
+ """Override to set up test resources."""
82
+ self._start_time = datetime.now(timezone.utc)
83
+ logger.info(f"Setting up test: {self.test_name}")
84
+
85
+ async def tearDown(self):
86
+ """Override for custom cleanup."""
87
+ logger.info(f"Tearing down test: {self.test_name}")
88
+
89
+ async def __aenter__(self):
90
+ """Async context manager entry."""
91
+ await self.setUp()
92
+ return self
93
+
94
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
95
+ """Async context manager exit with cleanup."""
96
+ # Always run tearDown
97
+ try:
98
+ await self.tearDown()
99
+ except Exception as e:
100
+ logger.error(f"Error in tearDown: {e}")
101
+
102
+ # Run all cleanup tasks in reverse order
103
+ for task in reversed(self._cleanup_tasks):
104
+ try:
105
+ if asyncio.iscoroutinefunction(task):
106
+ await task()
107
+ else:
108
+ task()
109
+ except Exception as e:
110
+ logger.error(f"Cleanup failed: {e}")
111
+
112
+ # Clean up registries
113
+ try:
114
+ await self.resource_registry.cleanup()
115
+ except Exception as e:
116
+ logger.error(f"Resource registry cleanup failed: {e}")
117
+
118
+ # Log test summary
119
+ if self._start_time:
120
+ duration = (datetime.now(timezone.utc) - self._start_time).total_seconds()
121
+ logger.info(
122
+ f"Test {self.test_name} completed in {duration:.2f}s "
123
+ f"with {self._assertions_made} assertions"
124
+ )
125
+
126
+ def add_cleanup(self, cleanup_func: Callable):
127
+ """Register a cleanup function/coroutine."""
128
+ self._cleanup_tasks.append(cleanup_func)
129
+
130
+ async def create_test_resource(
131
+ self,
132
+ name: str,
133
+ factory: Union[ResourceFactory, Callable],
134
+ mock: bool = False,
135
+ health_check: Callable = None,
136
+ cleanup_handler: Callable = None,
137
+ ) -> Any:
138
+ """Create a test resource with automatic cleanup."""
139
+ if mock:
140
+ # Create mock resource
141
+ resource = await self.mock_registry.create_mock(name, factory)
142
+ else:
143
+ # Handle callable factories
144
+ if callable(factory) and not hasattr(factory, "create"):
145
+ # Wrap in a proper factory
146
+ class CallableFactory:
147
+ def __init__(self, func):
148
+ self.func = func
149
+
150
+ async def create(self):
151
+ if asyncio.iscoroutinefunction(self.func):
152
+ return await self.func()
153
+ return self.func()
154
+
155
+ factory = CallableFactory(factory)
156
+
157
+ # Register real resource
158
+ self.resource_registry.register_factory(
159
+ name,
160
+ factory,
161
+ health_check=health_check,
162
+ cleanup_handler=cleanup_handler,
163
+ )
164
+ resource = await self.resource_registry.get_resource(name)
165
+
166
+ # Store for later access
167
+ self._test_resources[name] = resource
168
+
169
+ # Register cleanup if not already handled
170
+ if not cleanup_handler:
171
+
172
+ async def default_cleanup():
173
+ if hasattr(resource, "close"):
174
+ if asyncio.iscoroutinefunction(resource.close):
175
+ await resource.close()
176
+ else:
177
+ resource.close()
178
+ elif hasattr(resource, "cleanup"):
179
+ if asyncio.iscoroutinefunction(resource.cleanup):
180
+ await resource.cleanup()
181
+ else:
182
+ resource.cleanup()
183
+
184
+ self.add_cleanup(default_cleanup)
185
+
186
+ return resource
187
+
188
+ async def execute_workflow(
189
+ self,
190
+ workflow: Workflow,
191
+ inputs: Dict[str, Any],
192
+ mock_resources: Dict[str, Any] = None,
193
+ timeout: float = 30.0,
194
+ capture_logs: bool = True,
195
+ ) -> WorkflowTestResult:
196
+ """Execute workflow with test environment."""
197
+ # Register mock resources
198
+ if mock_resources:
199
+ for name, mock in mock_resources.items():
200
+ self.mock_registry.register_mock(name, mock)
201
+
202
+ # Create a test resource registry that checks mocks first
203
+ class TestResourceRegistry:
204
+ def __init__(self, real_registry, mock_registry):
205
+ self.real_registry = real_registry
206
+ self.mock_registry = mock_registry
207
+ # Copy all attributes from real registry to ensure compatibility
208
+ if real_registry:
209
+ for attr in dir(real_registry):
210
+ if not attr.startswith("_") and not hasattr(self, attr):
211
+ try:
212
+ setattr(self, attr, getattr(real_registry, attr))
213
+ except AttributeError:
214
+ pass
215
+
216
+ async def get_resource(self, name: str):
217
+ # Check mock registry first
218
+ mock = self.mock_registry.get_mock(name)
219
+ if mock is not None:
220
+ return mock
221
+ # Fall back to real resources
222
+ if self.real_registry:
223
+ return await self.real_registry.get_resource(name)
224
+ raise RuntimeError(f"No resource '{name}' found")
225
+
226
+ def register_factory(self, name: str, factory):
227
+ """Delegate factory registration to real registry."""
228
+ if self.real_registry:
229
+ return self.real_registry.register_factory(name, factory)
230
+
231
+ def list_resources(self):
232
+ """List all available resources."""
233
+ resources = []
234
+ if self.real_registry:
235
+ resources.extend(self.real_registry.list_resources())
236
+ resources.extend(self.mock_registry.list_mocks())
237
+ return resources
238
+
239
+ def __getattr__(self, name):
240
+ # Delegate other methods to real registry
241
+ if self.real_registry and hasattr(self.real_registry, name):
242
+ return getattr(self.real_registry, name)
243
+ raise AttributeError(
244
+ f"'{type(self).__name__}' has no attribute '{name}'"
245
+ )
246
+
247
+ # Create test runtime with test resource registry
248
+ test_registry = TestResourceRegistry(self.resource_registry, self.mock_registry)
249
+ runtime = AsyncLocalRuntime(resource_registry=test_registry)
250
+
251
+ # Execute with timeout
252
+ start_time = asyncio.get_event_loop().time()
253
+ logs = []
254
+
255
+ try:
256
+ # Execute workflow
257
+ result = await asyncio.wait_for(
258
+ runtime.execute_workflow_async(workflow, inputs), timeout=timeout
259
+ )
260
+
261
+ # Convert to test result
262
+ return WorkflowTestResult(
263
+ status="success" if not result.get("errors") else "failed",
264
+ outputs=result.get("results", {}),
265
+ errors=[
266
+ (node, error) for node, error in result.get("errors", {}).items()
267
+ ],
268
+ error=(
269
+ list(result.get("errors", {}).values())[0]
270
+ if result.get("errors")
271
+ else None
272
+ ),
273
+ execution_time=result.get(
274
+ "total_duration", asyncio.get_event_loop().time() - start_time
275
+ ),
276
+ node_timings=result.get("execution_times", {}),
277
+ logs=logs,
278
+ )
279
+
280
+ except asyncio.TimeoutError:
281
+ raise AssertionError(f"Workflow execution timed out after {timeout}s")
282
+ except Exception as e:
283
+ # Create error result
284
+ return WorkflowTestResult(
285
+ status="failed",
286
+ outputs={},
287
+ error=str(e),
288
+ errors=[("execution", e)],
289
+ execution_time=asyncio.get_event_loop().time() - start_time,
290
+ logs=logs,
291
+ )
292
+
293
+ def get_resource(self, name: str) -> Any:
294
+ """Get a test resource by name."""
295
+ if name in self._test_resources:
296
+ return self._test_resources[name]
297
+ raise KeyError(f"Test resource '{name}' not found")
298
+
299
+ # Assertion helpers
300
+ def assert_workflow_success(self, result: WorkflowTestResult):
301
+ """Assert workflow completed successfully."""
302
+ self._assertions_made += 1
303
+ assert result.status == "success", (
304
+ f"Workflow failed: {result.error}\n"
305
+ f"Errors: {result.errors}\n"
306
+ f"Logs: {result.get_logs()}"
307
+ )
308
+
309
+ def assert_workflow_failed(self, result: WorkflowTestResult):
310
+ """Assert workflow failed."""
311
+ self._assertions_made += 1
312
+ assert result.status == "failed", "Workflow did not fail as expected"
313
+
314
+ def assert_node_output(
315
+ self, result: WorkflowTestResult, node_id: str, expected: Any, key: str = None
316
+ ):
317
+ """Assert node output matches expected."""
318
+ self._assertions_made += 1
319
+ actual = result.get_output(node_id, key)
320
+ assert actual == expected, (
321
+ f"Node {node_id} output mismatch\n"
322
+ f"Expected: {expected}\n"
323
+ f"Actual: {actual}"
324
+ )
325
+
326
+ def assert_resource_called(
327
+ self,
328
+ resource_name: str,
329
+ method_name: str,
330
+ times: int = None,
331
+ with_args: tuple = None,
332
+ with_kwargs: dict = None,
333
+ ):
334
+ """Assert a resource method was called."""
335
+ self._assertions_made += 1
336
+ self.mock_registry.assert_called(
337
+ resource_name,
338
+ method_name,
339
+ times=times,
340
+ with_args=with_args,
341
+ with_kwargs=with_kwargs,
342
+ )
343
+
344
+ @asynccontextmanager
345
+ async def assert_time_limit(self, seconds: float):
346
+ """Context manager to assert code completes within time limit."""
347
+ start = asyncio.get_event_loop().time()
348
+ yield
349
+ elapsed = asyncio.get_event_loop().time() - start
350
+ self._assertions_made += 1
351
+ assert elapsed < seconds, (
352
+ f"Operation took {elapsed:.2f}s, " f"exceeding limit of {seconds}s"
353
+ )