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,417 @@
1
+ """Integration of durable request handling with API Gateway.
2
+
3
+ This module provides:
4
+ - Durable API Gateway with checkpointing
5
+ - Automatic request deduplication
6
+ - Event sourcing integration
7
+ - Backward compatibility with existing gateway
8
+ """
9
+
10
+ import asyncio
11
+ import logging
12
+ from datetime import UTC, datetime
13
+ from typing import Any, Callable, Dict, List, Optional
14
+
15
+ from fastapi import HTTPException, Request, Response
16
+ from fastapi.responses import JSONResponse
17
+
18
+ from kailash.api.gateway import WorkflowAPIGateway
19
+
20
+ from .checkpoint_manager import CheckpointManager
21
+ from .deduplicator import RequestDeduplicator
22
+ from .durable_request import DurableRequest, RequestMetadata, RequestState
23
+ from .event_store import (
24
+ EventStore,
25
+ EventType,
26
+ performance_metrics_projection,
27
+ request_state_projection,
28
+ )
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class DurableAPIGateway(WorkflowAPIGateway):
34
+ """API Gateway with durable request handling.
35
+
36
+ Extends the standard gateway with:
37
+ - Request durability and checkpointing
38
+ - Automatic deduplication
39
+ - Event sourcing for audit trail
40
+ - Long-running request support
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ title: str = "Kailash Durable Workflow Gateway",
46
+ description: str = "Durable API for Kailash workflows",
47
+ version: str = "1.0.0",
48
+ max_workers: int = 10,
49
+ cors_origins: Optional[list[str]] = None,
50
+ # Durability configuration
51
+ enable_durability: bool = True,
52
+ checkpoint_manager: Optional[CheckpointManager] = None,
53
+ deduplicator: Optional[RequestDeduplicator] = None,
54
+ event_store: Optional[EventStore] = None,
55
+ durability_opt_in: bool = True, # If True, durability is opt-in per endpoint
56
+ ):
57
+ """Initialize durable API gateway."""
58
+ super().__init__(
59
+ title=title,
60
+ description=description,
61
+ version=version,
62
+ max_workers=max_workers,
63
+ cors_origins=cors_origins,
64
+ )
65
+
66
+ # Durability components
67
+ self.enable_durability = enable_durability
68
+ self.durability_opt_in = durability_opt_in
69
+ self.checkpoint_manager = checkpoint_manager or CheckpointManager()
70
+ self.deduplicator = deduplicator or RequestDeduplicator()
71
+ self.event_store = event_store or EventStore()
72
+
73
+ # Track active requests
74
+ self.active_requests: Dict[str, DurableRequest] = {}
75
+
76
+ # Register event projections
77
+ self.event_store.register_projection(
78
+ "request_states",
79
+ request_state_projection,
80
+ )
81
+ self.event_store.register_projection(
82
+ "performance_metrics",
83
+ performance_metrics_projection,
84
+ )
85
+
86
+ # Add durability middleware if enabled
87
+ if self.enable_durability:
88
+ self._add_durability_middleware()
89
+
90
+ # Register durability endpoints
91
+ self._register_durability_endpoints()
92
+
93
+ # Track background tasks
94
+ self._background_tasks: List[asyncio.Task] = []
95
+
96
+ def _add_durability_middleware(self):
97
+ """Add middleware for durable request handling."""
98
+
99
+ @self.app.middleware("http")
100
+ async def durability_middleware(request: Request, call_next):
101
+ """Process requests with durability support."""
102
+ # Check if durability is enabled for this endpoint
103
+ if not self._should_use_durability(request):
104
+ return await call_next(request)
105
+
106
+ # Extract request metadata
107
+ metadata = await self._extract_metadata(request)
108
+
109
+ # Check for duplicate request
110
+ duplicate_response = await self._check_duplicate(request, metadata)
111
+ if duplicate_response:
112
+ return duplicate_response
113
+
114
+ # Create durable request
115
+ durable_request = DurableRequest(
116
+ metadata=metadata,
117
+ checkpoint_manager=self.checkpoint_manager,
118
+ )
119
+
120
+ # Track active request
121
+ self.active_requests[durable_request.id] = durable_request
122
+
123
+ try:
124
+ # Record request creation
125
+ await self.event_store.append(
126
+ EventType.REQUEST_CREATED,
127
+ durable_request.id,
128
+ {
129
+ "method": metadata.method,
130
+ "path": metadata.path,
131
+ "idempotency_key": metadata.idempotency_key,
132
+ },
133
+ )
134
+
135
+ # Execute with durability
136
+ response = await self._execute_durable_request(
137
+ durable_request,
138
+ request,
139
+ call_next,
140
+ )
141
+
142
+ # Cache response for deduplication
143
+ await self._cache_response(request, metadata, response)
144
+
145
+ return response
146
+
147
+ finally:
148
+ # Clean up active request
149
+ del self.active_requests[durable_request.id]
150
+
151
+ def _should_use_durability(self, request: Request) -> bool:
152
+ """Check if durability should be used for this request."""
153
+ if not self.enable_durability:
154
+ return False
155
+
156
+ if self.durability_opt_in:
157
+ # Check for durability header or query param
158
+ use_durability = (
159
+ request.headers.get("X-Durable-Request", "").lower() == "true"
160
+ or request.query_params.get("durable", "").lower() == "true"
161
+ )
162
+ return use_durability
163
+
164
+ # Durability enabled for all requests
165
+ return True
166
+
167
+ async def _extract_metadata(self, request: Request) -> RequestMetadata:
168
+ """Extract metadata from HTTP request."""
169
+ # Get body if present
170
+ body = None
171
+ if request.method in ["POST", "PUT", "PATCH"]:
172
+ try:
173
+ body = await request.json()
174
+ except:
175
+ pass
176
+
177
+ # Extract user/tenant from headers or auth
178
+ user_id = request.headers.get("X-User-ID")
179
+ tenant_id = request.headers.get("X-Tenant-ID")
180
+
181
+ # Get idempotency key
182
+ idempotency_key = request.headers.get("Idempotency-Key") or request.headers.get(
183
+ "X-Idempotency-Key"
184
+ )
185
+
186
+ return RequestMetadata(
187
+ request_id=f"req_{request.headers.get('X-Request-ID', '')}",
188
+ method=request.method,
189
+ path=str(request.url.path),
190
+ headers=dict(request.headers),
191
+ query_params=dict(request.query_params),
192
+ body=body,
193
+ client_ip=request.client.host if request.client else "0.0.0.0",
194
+ user_id=user_id,
195
+ tenant_id=tenant_id,
196
+ idempotency_key=idempotency_key,
197
+ created_at=datetime.now(UTC),
198
+ updated_at=datetime.now(UTC),
199
+ )
200
+
201
+ async def _check_duplicate(
202
+ self,
203
+ request: Request,
204
+ metadata: RequestMetadata,
205
+ ) -> Optional[Response]:
206
+ """Check for duplicate request."""
207
+ duplicate = await self.deduplicator.check_duplicate(
208
+ method=metadata.method,
209
+ path=metadata.path,
210
+ query_params=metadata.query_params,
211
+ body=metadata.body,
212
+ headers=metadata.headers,
213
+ idempotency_key=metadata.idempotency_key,
214
+ )
215
+
216
+ if duplicate:
217
+ # Record deduplication hit
218
+ await self.event_store.append(
219
+ EventType.DEDUPLICATION_HIT,
220
+ metadata.request_id,
221
+ {
222
+ "cached_response": True,
223
+ "cache_age_seconds": duplicate["cache_age_seconds"],
224
+ },
225
+ )
226
+
227
+ return JSONResponse(
228
+ content=duplicate["data"],
229
+ status_code=duplicate["status_code"],
230
+ headers={
231
+ **duplicate["headers"],
232
+ "X-Cached-Response": "true",
233
+ "X-Cache-Age": str(duplicate["cache_age_seconds"]),
234
+ },
235
+ )
236
+
237
+ return None
238
+
239
+ async def _execute_durable_request(
240
+ self,
241
+ durable_request: DurableRequest,
242
+ request: Request,
243
+ call_next: Callable,
244
+ ) -> Response:
245
+ """Execute request with durability."""
246
+ try:
247
+ # Convert HTTP request to workflow request
248
+ # This is simplified - real implementation would parse the request
249
+ # and create appropriate workflow based on routing
250
+
251
+ # For now, just execute the request normally
252
+ response = await call_next(request)
253
+
254
+ # Record completion
255
+ await self.event_store.append(
256
+ EventType.REQUEST_COMPLETED,
257
+ durable_request.id,
258
+ {
259
+ "status_code": response.status_code,
260
+ "duration_ms": 0, # TODO: Track actual duration
261
+ },
262
+ )
263
+
264
+ return response
265
+
266
+ except Exception as e:
267
+ # Record failure
268
+ await self.event_store.append(
269
+ EventType.REQUEST_FAILED,
270
+ durable_request.id,
271
+ {
272
+ "error": str(e),
273
+ "error_type": type(e).__name__,
274
+ },
275
+ )
276
+ raise
277
+
278
+ async def _cache_response(
279
+ self,
280
+ request: Request,
281
+ metadata: RequestMetadata,
282
+ response: Response,
283
+ ):
284
+ """Cache response for deduplication."""
285
+ # Only cache successful responses
286
+ if response.status_code >= 400:
287
+ return
288
+
289
+ # Extract response data
290
+ response_data = {}
291
+ if hasattr(response, "body"):
292
+ try:
293
+ # Decode response body
294
+ import json
295
+
296
+ response_data = json.loads(response.body)
297
+ except:
298
+ pass
299
+
300
+ await self.deduplicator.cache_response(
301
+ method=metadata.method,
302
+ path=metadata.path,
303
+ query_params=metadata.query_params,
304
+ body=metadata.body,
305
+ headers=metadata.headers,
306
+ idempotency_key=metadata.idempotency_key,
307
+ response_data=response_data,
308
+ status_code=response.status_code,
309
+ response_headers=(
310
+ dict(response.headers) if hasattr(response, "headers") else {}
311
+ ),
312
+ )
313
+
314
+ def _register_durability_endpoints(self):
315
+ """Register durability-specific endpoints."""
316
+
317
+ @self.app.get("/durability/status")
318
+ async def durability_status():
319
+ """Get durability system status."""
320
+ return {
321
+ "enabled": self.enable_durability,
322
+ "opt_in": self.durability_opt_in,
323
+ "active_requests": len(self.active_requests),
324
+ "checkpoint_stats": self.checkpoint_manager.get_stats(),
325
+ "deduplication_stats": self.deduplicator.get_stats(),
326
+ "event_store_stats": self.event_store.get_stats(),
327
+ }
328
+
329
+ @self.app.get("/durability/requests/{request_id}")
330
+ async def get_request_status(request_id: str):
331
+ """Get status of a durable request."""
332
+ # Check active requests
333
+ if request_id in self.active_requests:
334
+ return self.active_requests[request_id].get_status()
335
+
336
+ # Check event store for historical data
337
+ events = await self.event_store.get_events(request_id)
338
+ if not events:
339
+ raise HTTPException(status_code=404, detail="Request not found")
340
+
341
+ # Build status from events
342
+ status = {
343
+ "request_id": request_id,
344
+ "events": len(events),
345
+ "first_event": events[0].timestamp.isoformat(),
346
+ "last_event": events[-1].timestamp.isoformat(),
347
+ "state": "unknown",
348
+ }
349
+
350
+ # Determine final state
351
+ for event in reversed(events):
352
+ if event.event_type == EventType.REQUEST_COMPLETED:
353
+ status["state"] = "completed"
354
+ break
355
+ elif event.event_type == EventType.REQUEST_FAILED:
356
+ status["state"] = "failed"
357
+ break
358
+ elif event.event_type == EventType.REQUEST_CANCELLED:
359
+ status["state"] = "cancelled"
360
+ break
361
+
362
+ return status
363
+
364
+ @self.app.get("/durability/requests/{request_id}/events")
365
+ async def get_request_events(request_id: str):
366
+ """Get all events for a request."""
367
+ events = await self.event_store.get_events(request_id)
368
+ return {
369
+ "request_id": request_id,
370
+ "event_count": len(events),
371
+ "events": [e.to_dict() for e in events],
372
+ }
373
+
374
+ @self.app.post("/durability/requests/{request_id}/resume")
375
+ async def resume_request(request_id: str, checkpoint_id: Optional[str] = None):
376
+ """Resume a failed or incomplete request."""
377
+ # TODO: Implement request resumption
378
+ return {
379
+ "status": "not_implemented",
380
+ "message": "Request resumption coming soon",
381
+ }
382
+
383
+ @self.app.delete("/durability/requests/{request_id}")
384
+ async def cancel_request(request_id: str):
385
+ """Cancel an active request."""
386
+ if request_id not in self.active_requests:
387
+ raise HTTPException(status_code=404, detail="Active request not found")
388
+
389
+ durable_request = self.active_requests[request_id]
390
+ await durable_request.cancel()
391
+
392
+ return {"status": "cancelled", "request_id": request_id}
393
+
394
+ @self.app.get("/durability/projections/{name}")
395
+ async def get_projection(name: str):
396
+ """Get current state of a projection."""
397
+ projection = self.event_store.get_projection(name)
398
+ if projection is None:
399
+ raise HTTPException(status_code=404, detail="Projection not found")
400
+
401
+ return {
402
+ "name": name,
403
+ "state": projection,
404
+ }
405
+
406
+ async def close(self):
407
+ """Close the durable gateway and cleanup resources."""
408
+ await self.checkpoint_manager.close()
409
+ await self.deduplicator.close()
410
+ await self.event_store.close()
411
+
412
+ # Wait for active requests to complete
413
+ if self.active_requests:
414
+ logger.info(f"Waiting for {len(self.active_requests)} active requests")
415
+ # TODO: Implement graceful shutdown with timeout
416
+
417
+ # No parent close() method to call