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,319 @@
1
+ """
2
+ Health Check System - Monitor and maintain resource health.
3
+
4
+ This module provides health checking functionality for resources in the registry,
5
+ enabling automatic recovery and circuit breaker patterns.
6
+ """
7
+
8
+ import asyncio
9
+ from dataclasses import dataclass
10
+ from datetime import datetime
11
+ from enum import Enum
12
+ from typing import Any, Dict, Optional, Protocol, Union
13
+
14
+
15
+ class HealthCheckProtocol(Protocol):
16
+ """Protocol for health check callables."""
17
+
18
+ async def __call__(self, resource: Any) -> Union[bool, "HealthStatus"]:
19
+ """
20
+ Check if a resource is healthy.
21
+
22
+ Args:
23
+ resource: The resource to check
24
+
25
+ Returns:
26
+ bool or HealthStatus indicating health
27
+ """
28
+ ...
29
+
30
+
31
+ # Type alias for health checks
32
+ HealthCheck = HealthCheckProtocol
33
+
34
+
35
+ class HealthState(Enum):
36
+ """Health states for resources."""
37
+
38
+ HEALTHY = "healthy"
39
+ DEGRADED = "degraded"
40
+ UNHEALTHY = "unhealthy"
41
+ UNKNOWN = "unknown"
42
+
43
+
44
+ @dataclass
45
+ class HealthStatus:
46
+ """
47
+ Detailed health status for a resource.
48
+
49
+ This provides more information than a simple boolean,
50
+ allowing for degraded states and detailed diagnostics.
51
+ """
52
+
53
+ state: HealthState
54
+ message: Optional[str] = None
55
+ details: Optional[Dict[str, Any]] = None
56
+ timestamp: datetime = None
57
+
58
+ def __post_init__(self):
59
+ if self.timestamp is None:
60
+ self.timestamp = datetime.now()
61
+
62
+ @property
63
+ def is_healthy(self) -> bool:
64
+ """Check if status indicates health."""
65
+ return self.state in (HealthState.HEALTHY, HealthState.DEGRADED)
66
+
67
+ @classmethod
68
+ def healthy(cls, message: str = "Resource is healthy") -> "HealthStatus":
69
+ """Create a healthy status."""
70
+ return cls(HealthState.HEALTHY, message)
71
+
72
+ @classmethod
73
+ def unhealthy(cls, message: str, details: Dict[str, Any] = None) -> "HealthStatus":
74
+ """Create an unhealthy status."""
75
+ return cls(HealthState.UNHEALTHY, message, details)
76
+
77
+ @classmethod
78
+ def degraded(cls, message: str, details: Dict[str, Any] = None) -> "HealthStatus":
79
+ """Create a degraded status."""
80
+ return cls(HealthState.DEGRADED, message, details)
81
+
82
+
83
+ # Common health checks
84
+
85
+
86
+ async def database_health_check(pool) -> HealthStatus:
87
+ """
88
+ Health check for database connection pools.
89
+
90
+ Args:
91
+ pool: Database connection pool
92
+
93
+ Returns:
94
+ HealthStatus indicating database health
95
+ """
96
+ try:
97
+ # Try to acquire a connection
98
+ if hasattr(pool, "acquire"):
99
+ # asyncpg style
100
+ async with pool.acquire() as conn:
101
+ # Try a simple query
102
+ if hasattr(conn, "fetchval"):
103
+ await conn.fetchval("SELECT 1")
104
+ elif hasattr(conn, "execute"):
105
+ await conn.execute("SELECT 1")
106
+ elif hasattr(pool, "ping"):
107
+ # Some pools have a ping method
108
+ await pool.ping()
109
+ elif hasattr(pool, "execute"):
110
+ # aiosqlite style
111
+ await pool.execute("SELECT 1")
112
+
113
+ return HealthStatus.healthy("Database connection is healthy")
114
+
115
+ except asyncio.TimeoutError:
116
+ return HealthStatus.unhealthy(
117
+ "Database connection timed out", {"error": "timeout"}
118
+ )
119
+ except Exception as e:
120
+ return HealthStatus.unhealthy(
121
+ f"Database health check failed: {str(e)}",
122
+ {"error": str(e), "type": type(e).__name__},
123
+ )
124
+
125
+
126
+ async def http_client_health_check(
127
+ client, health_endpoint: str = "/health"
128
+ ) -> HealthStatus:
129
+ """
130
+ Health check for HTTP clients.
131
+
132
+ Args:
133
+ client: HTTP client (aiohttp or httpx)
134
+ health_endpoint: Endpoint to check
135
+
136
+ Returns:
137
+ HealthStatus indicating HTTP client health
138
+ """
139
+ try:
140
+ # Handle different client types
141
+ if hasattr(client, "get"):
142
+ # aiohttp or httpx style
143
+ if hasattr(client, "_base_url"):
144
+ # httpx
145
+ response = await client.get(health_endpoint)
146
+ status_code = response.status_code
147
+ else:
148
+ # aiohttp
149
+ async with client.get(health_endpoint) as response:
150
+ status_code = response.status
151
+
152
+ if 200 <= status_code < 300:
153
+ return HealthStatus.healthy(
154
+ f"HTTP client is healthy (status: {status_code})"
155
+ )
156
+ else:
157
+ return HealthStatus.unhealthy(
158
+ f"HTTP health check returned {status_code}",
159
+ {"status_code": status_code},
160
+ )
161
+ else:
162
+ # Unknown client type, assume healthy
163
+ return HealthStatus.healthy(
164
+ "HTTP client type not recognized, assuming healthy"
165
+ )
166
+
167
+ except asyncio.TimeoutError:
168
+ return HealthStatus.unhealthy(
169
+ "HTTP health check timed out", {"error": "timeout"}
170
+ )
171
+ except Exception as e:
172
+ return HealthStatus.unhealthy(
173
+ f"HTTP health check failed: {str(e)}",
174
+ {"error": str(e), "type": type(e).__name__},
175
+ )
176
+
177
+
178
+ async def cache_health_check(cache) -> HealthStatus:
179
+ """
180
+ Health check for cache clients.
181
+
182
+ Args:
183
+ cache: Cache client (Redis, Memcached, etc.)
184
+
185
+ Returns:
186
+ HealthStatus indicating cache health
187
+ """
188
+ try:
189
+ test_key = "_health_check_test"
190
+ test_value = "healthy"
191
+
192
+ # Try to set and get a value
193
+ if hasattr(cache, "set") and hasattr(cache, "get"):
194
+ await cache.set(test_key, test_value)
195
+ result = await cache.get(test_key)
196
+
197
+ if result == test_value or (
198
+ isinstance(result, bytes) and result.decode() == test_value
199
+ ):
200
+ # Clean up
201
+ if hasattr(cache, "delete"):
202
+ await cache.delete(test_key)
203
+ return HealthStatus.healthy("Cache is healthy")
204
+ else:
205
+ return HealthStatus.unhealthy(
206
+ "Cache health check failed: value mismatch",
207
+ {"expected": test_value, "got": result},
208
+ )
209
+ elif hasattr(cache, "ping"):
210
+ # Redis style ping
211
+ await cache.ping()
212
+ return HealthStatus.healthy("Cache is healthy (ping successful)")
213
+ else:
214
+ # Unknown cache type
215
+ return HealthStatus.healthy("Cache type not recognized, assuming healthy")
216
+
217
+ except asyncio.TimeoutError:
218
+ return HealthStatus.unhealthy(
219
+ "Cache health check timed out", {"error": "timeout"}
220
+ )
221
+ except Exception as e:
222
+ return HealthStatus.unhealthy(
223
+ f"Cache health check failed: {str(e)}",
224
+ {"error": str(e), "type": type(e).__name__},
225
+ )
226
+
227
+
228
+ async def message_queue_health_check(mq) -> HealthStatus:
229
+ """
230
+ Health check for message queue clients.
231
+
232
+ Args:
233
+ mq: Message queue client
234
+
235
+ Returns:
236
+ HealthStatus indicating message queue health
237
+ """
238
+ try:
239
+ # RabbitMQ (aio-pika)
240
+ if hasattr(mq, "channel"):
241
+ channel = await mq.channel()
242
+ await channel.close()
243
+ return HealthStatus.healthy(
244
+ "Message queue is healthy (channel test successful)"
245
+ )
246
+
247
+ # Kafka
248
+ elif hasattr(mq, "producer") and hasattr(mq, "consumer"):
249
+ # Custom Kafka client from factory
250
+ # Just check if they exist
251
+ return HealthStatus.healthy("Kafka clients are healthy")
252
+
253
+ # Generic check
254
+ elif hasattr(mq, "is_closed"):
255
+ if not mq.is_closed:
256
+ return HealthStatus.healthy("Message queue connection is open")
257
+ else:
258
+ return HealthStatus.unhealthy("Message queue connection is closed")
259
+
260
+ else:
261
+ # Unknown MQ type
262
+ return HealthStatus.healthy(
263
+ "Message queue type not recognized, assuming healthy"
264
+ )
265
+
266
+ except asyncio.TimeoutError:
267
+ return HealthStatus.unhealthy(
268
+ "Message queue health check timed out", {"error": "timeout"}
269
+ )
270
+ except Exception as e:
271
+ return HealthStatus.unhealthy(
272
+ f"Message queue health check failed: {str(e)}",
273
+ {"error": str(e), "type": type(e).__name__},
274
+ )
275
+
276
+
277
+ def create_composite_health_check(*checks: HealthCheck) -> HealthCheck:
278
+ """
279
+ Create a composite health check from multiple checks.
280
+
281
+ All checks must pass for the resource to be considered healthy.
282
+
283
+ Args:
284
+ *checks: Health check functions
285
+
286
+ Returns:
287
+ Composite health check function
288
+
289
+ Example:
290
+ ```python
291
+ health_check = create_composite_health_check(
292
+ lambda r: database_health_check(r.db),
293
+ lambda r: cache_health_check(r.cache)
294
+ )
295
+ ```
296
+ """
297
+
298
+ async def composite_check(resource: Any) -> HealthStatus:
299
+ results = []
300
+
301
+ for check in checks:
302
+ try:
303
+ result = await check(resource)
304
+ if isinstance(result, bool):
305
+ if not result:
306
+ return HealthStatus.unhealthy("Composite check failed")
307
+ elif isinstance(result, HealthStatus):
308
+ if not result.is_healthy:
309
+ return result
310
+ results.append(result)
311
+ except Exception as e:
312
+ return HealthStatus.unhealthy(
313
+ f"Health check error: {str(e)}", {"error": str(e)}
314
+ )
315
+
316
+ # All checks passed
317
+ return HealthStatus.healthy("All health checks passed")
318
+
319
+ return composite_check
@@ -0,0 +1,288 @@
1
+ """
2
+ Resource References - JSON-serializable references to resources.
3
+
4
+ This module enables resources to be referenced in JSON APIs by providing
5
+ a serializable reference format.
6
+ """
7
+
8
+ import json
9
+ from dataclasses import asdict, dataclass
10
+ from typing import Any, Dict, Optional
11
+
12
+
13
+ @dataclass
14
+ class ResourceReference:
15
+ """
16
+ Reference to a resource that can be resolved by the gateway.
17
+
18
+ This class provides a JSON-serializable way to reference resources
19
+ in API calls, solving the problem of passing non-serializable objects.
20
+
21
+ Example:
22
+ ```python
23
+ # Create a reference to a database
24
+ db_ref = ResourceReference(
25
+ type="database",
26
+ config={
27
+ "host": "localhost",
28
+ "database": "myapp"
29
+ },
30
+ credentials_ref="db_credentials"
31
+ )
32
+
33
+ # Convert to JSON
34
+ json_ref = db_ref.to_json()
35
+
36
+ # Use in API call
37
+ response = await client.execute_workflow(
38
+ workflow_id="process_data",
39
+ inputs={"data": [1, 2, 3]},
40
+ resources={"db": db_ref}
41
+ )
42
+ ```
43
+
44
+ Attributes:
45
+ type: The type of resource (database, http_client, cache, etc.)
46
+ config: Configuration parameters for the resource
47
+ credentials_ref: Optional reference to credentials in secret manager
48
+ name: Optional name to reference a pre-registered resource
49
+ """
50
+
51
+ type: str
52
+ config: Dict[str, Any]
53
+ credentials_ref: Optional[str] = None
54
+ name: Optional[str] = None
55
+
56
+ def to_dict(self) -> Dict[str, Any]:
57
+ """
58
+ Convert to JSON-serializable dictionary.
59
+
60
+ Returns:
61
+ Dictionary representation of the reference
62
+ """
63
+ data = {"type": self.type, "config": self.config}
64
+
65
+ if self.credentials_ref:
66
+ data["credentials_ref"] = self.credentials_ref
67
+
68
+ if self.name:
69
+ data["name"] = self.name
70
+
71
+ return data
72
+
73
+ def to_json(self) -> str:
74
+ """
75
+ Convert to JSON string.
76
+
77
+ Returns:
78
+ JSON string representation
79
+ """
80
+ return json.dumps(self.to_dict())
81
+
82
+ @classmethod
83
+ def from_dict(cls, data: Dict[str, Any]) -> "ResourceReference":
84
+ """
85
+ Create from dictionary.
86
+
87
+ Args:
88
+ data: Dictionary with reference data
89
+
90
+ Returns:
91
+ ResourceReference instance
92
+ """
93
+ return cls(
94
+ type=data["type"],
95
+ config=data["config"],
96
+ credentials_ref=data.get("credentials_ref"),
97
+ name=data.get("name"),
98
+ )
99
+
100
+ @classmethod
101
+ def from_json(cls, json_str: str) -> "ResourceReference":
102
+ """
103
+ Create from JSON string.
104
+
105
+ Args:
106
+ json_str: JSON string representation
107
+
108
+ Returns:
109
+ ResourceReference instance
110
+ """
111
+ return cls.from_dict(json.loads(json_str))
112
+
113
+ @classmethod
114
+ def for_registered_resource(cls, name: str) -> "ResourceReference":
115
+ """
116
+ Create a reference to a pre-registered resource.
117
+
118
+ This is a shorthand for referencing resources that are already
119
+ registered in the ResourceRegistry.
120
+
121
+ Args:
122
+ name: Name of the registered resource
123
+
124
+ Returns:
125
+ ResourceReference instance
126
+
127
+ Example:
128
+ ```python
129
+ # Reference a pre-registered database
130
+ db_ref = ResourceReference.for_registered_resource("main_db")
131
+ ```
132
+ """
133
+ return cls(type="registered", config={}, name=name)
134
+
135
+
136
+ def create_database_reference(
137
+ host: str,
138
+ database: str,
139
+ backend: str = "postgresql",
140
+ port: Optional[int] = None,
141
+ credentials_ref: Optional[str] = None,
142
+ **kwargs,
143
+ ) -> ResourceReference:
144
+ """
145
+ Helper to create a database resource reference.
146
+
147
+ Args:
148
+ host: Database host
149
+ database: Database name
150
+ backend: Database backend (postgresql, mysql, sqlite)
151
+ port: Database port (uses default if not specified)
152
+ credentials_ref: Reference to credentials in secret manager
153
+ **kwargs: Additional configuration
154
+
155
+ Returns:
156
+ ResourceReference for a database
157
+
158
+ Example:
159
+ ```python
160
+ db_ref = create_database_reference(
161
+ host="db.example.com",
162
+ database="production",
163
+ credentials_ref="prod_db_creds"
164
+ )
165
+ ```
166
+ """
167
+ config = {"backend": backend, "host": host, "database": database, **kwargs}
168
+
169
+ if port:
170
+ config["port"] = port
171
+
172
+ return ResourceReference(
173
+ type="database", config=config, credentials_ref=credentials_ref
174
+ )
175
+
176
+
177
+ def create_http_client_reference(
178
+ base_url: str,
179
+ backend: str = "aiohttp",
180
+ timeout: int = 30,
181
+ credentials_ref: Optional[str] = None,
182
+ headers: Optional[Dict[str, str]] = None,
183
+ **kwargs,
184
+ ) -> ResourceReference:
185
+ """
186
+ Helper to create an HTTP client resource reference.
187
+
188
+ Args:
189
+ base_url: Base URL for the HTTP client
190
+ backend: HTTP client backend (aiohttp, httpx)
191
+ timeout: Request timeout in seconds
192
+ credentials_ref: Reference to credentials for auth headers
193
+ headers: Additional headers
194
+ **kwargs: Additional configuration
195
+
196
+ Returns:
197
+ ResourceReference for an HTTP client
198
+
199
+ Example:
200
+ ```python
201
+ api_ref = create_http_client_reference(
202
+ base_url="https://api.example.com",
203
+ timeout=60,
204
+ credentials_ref="api_key"
205
+ )
206
+ ```
207
+ """
208
+ config = {"backend": backend, "base_url": base_url, "timeout": timeout, **kwargs}
209
+
210
+ if headers:
211
+ config["headers"] = headers
212
+
213
+ return ResourceReference(
214
+ type="http_client", config=config, credentials_ref=credentials_ref
215
+ )
216
+
217
+
218
+ def create_cache_reference(
219
+ backend: str = "redis",
220
+ host: str = "localhost",
221
+ port: Optional[int] = None,
222
+ **kwargs,
223
+ ) -> ResourceReference:
224
+ """
225
+ Helper to create a cache resource reference.
226
+
227
+ Args:
228
+ backend: Cache backend (redis, memcached, memory)
229
+ host: Cache server host
230
+ port: Cache server port
231
+ **kwargs: Additional configuration
232
+
233
+ Returns:
234
+ ResourceReference for a cache
235
+
236
+ Example:
237
+ ```python
238
+ cache_ref = create_cache_reference(
239
+ backend="redis",
240
+ host="cache.example.com"
241
+ )
242
+ ```
243
+ """
244
+ config = {"backend": backend, "host": host, **kwargs}
245
+
246
+ if port:
247
+ config["port"] = port
248
+
249
+ return ResourceReference(type="cache", config=config)
250
+
251
+
252
+ def create_message_queue_reference(
253
+ backend: str,
254
+ host: str = "localhost",
255
+ port: Optional[int] = None,
256
+ credentials_ref: Optional[str] = None,
257
+ **kwargs,
258
+ ) -> ResourceReference:
259
+ """
260
+ Helper to create a message queue resource reference.
261
+
262
+ Args:
263
+ backend: Message queue backend (rabbitmq, kafka, redis)
264
+ host: Message queue host
265
+ port: Message queue port
266
+ credentials_ref: Reference to credentials
267
+ **kwargs: Additional configuration
268
+
269
+ Returns:
270
+ ResourceReference for a message queue
271
+
272
+ Example:
273
+ ```python
274
+ mq_ref = create_message_queue_reference(
275
+ backend="rabbitmq",
276
+ host="mq.example.com",
277
+ credentials_ref="mq_creds"
278
+ )
279
+ ```
280
+ """
281
+ config = {"backend": backend, "host": host, **kwargs}
282
+
283
+ if port:
284
+ config["port"] = port
285
+
286
+ return ResourceReference(
287
+ type="message_queue", config=config, credentials_ref=credentials_ref
288
+ )