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.
Files changed (64) 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/database/repositories.py +3 -1
  19. kailash/middleware/gateway/__init__.py +22 -0
  20. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  21. kailash/middleware/gateway/deduplicator.py +382 -0
  22. kailash/middleware/gateway/durable_gateway.py +417 -0
  23. kailash/middleware/gateway/durable_request.py +498 -0
  24. kailash/middleware/gateway/event_store.py +459 -0
  25. kailash/nodes/admin/audit_log.py +364 -6
  26. kailash/nodes/admin/permission_check.py +817 -33
  27. kailash/nodes/admin/role_management.py +1242 -108
  28. kailash/nodes/admin/schema_manager.py +438 -0
  29. kailash/nodes/admin/user_management.py +1209 -681
  30. kailash/nodes/api/http.py +95 -71
  31. kailash/nodes/base.py +281 -164
  32. kailash/nodes/base_async.py +30 -31
  33. kailash/nodes/code/__init__.py +8 -1
  34. kailash/nodes/code/async_python.py +1035 -0
  35. kailash/nodes/code/python.py +1 -0
  36. kailash/nodes/data/async_sql.py +12 -25
  37. kailash/nodes/data/sql.py +20 -11
  38. kailash/nodes/data/workflow_connection_pool.py +643 -0
  39. kailash/nodes/rag/__init__.py +1 -4
  40. kailash/resources/__init__.py +40 -0
  41. kailash/resources/factory.py +533 -0
  42. kailash/resources/health.py +319 -0
  43. kailash/resources/reference.py +288 -0
  44. kailash/resources/registry.py +392 -0
  45. kailash/runtime/async_local.py +711 -302
  46. kailash/testing/__init__.py +34 -0
  47. kailash/testing/async_test_case.py +353 -0
  48. kailash/testing/async_utils.py +345 -0
  49. kailash/testing/fixtures.py +458 -0
  50. kailash/testing/mock_registry.py +495 -0
  51. kailash/utils/resource_manager.py +420 -0
  52. kailash/workflow/__init__.py +8 -0
  53. kailash/workflow/async_builder.py +621 -0
  54. kailash/workflow/async_patterns.py +766 -0
  55. kailash/workflow/builder.py +93 -10
  56. kailash/workflow/cyclic_runner.py +111 -41
  57. kailash/workflow/graph.py +7 -2
  58. kailash/workflow/resilience.py +11 -1
  59. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
  60. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
  61. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  62. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  63. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  64. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,498 @@
1
+ """Durable request implementation with state machine and checkpointing.
2
+
3
+ This module provides request durability through:
4
+ - State machine for request lifecycle
5
+ - Automatic checkpointing at key points
6
+ - Execution journal for audit trail
7
+ - Resumable execution after failures
8
+ """
9
+
10
+ import asyncio
11
+ import datetime as dt
12
+ import json
13
+ import logging
14
+ import time
15
+ import uuid
16
+ from dataclasses import dataclass, field
17
+ from datetime import datetime
18
+ from enum import Enum
19
+
20
+ # Import will be added when checkpoint_manager is implemented
21
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Optional
22
+
23
+ from kailash.runtime import LocalRuntime
24
+
25
+ if TYPE_CHECKING:
26
+ from .checkpoint_manager import CheckpointManager
27
+
28
+ from kailash.sdk_exceptions import NodeExecutionError
29
+ from kailash.workflow import Workflow, WorkflowBuilder
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class RequestState(Enum):
35
+ """Request lifecycle states."""
36
+
37
+ INITIALIZED = "initialized"
38
+ VALIDATED = "validated"
39
+ WORKFLOW_CREATED = "workflow_created"
40
+ EXECUTING = "executing"
41
+ CHECKPOINTED = "checkpointed"
42
+ COMPLETED = "completed"
43
+ FAILED = "failed"
44
+ CANCELLED = "cancelled"
45
+ RESUMING = "resuming"
46
+
47
+
48
+ @dataclass
49
+ class RequestMetadata:
50
+ """Metadata for durable request."""
51
+
52
+ request_id: str
53
+ method: str
54
+ path: str
55
+ headers: Dict[str, str]
56
+ query_params: Dict[str, str]
57
+ body: Optional[Dict[str, Any]]
58
+ client_ip: str
59
+ user_id: Optional[str]
60
+ tenant_id: Optional[str]
61
+ idempotency_key: Optional[str]
62
+ created_at: datetime
63
+ updated_at: datetime
64
+
65
+
66
+ @dataclass
67
+ class Checkpoint:
68
+ """Checkpoint data structure."""
69
+
70
+ checkpoint_id: str
71
+ request_id: str
72
+ sequence: int
73
+ name: str
74
+ state: RequestState
75
+ data: Dict[str, Any]
76
+ workflow_state: Optional[Dict[str, Any]]
77
+ created_at: datetime
78
+ size_bytes: int
79
+
80
+ def to_dict(self) -> Dict[str, Any]:
81
+ """Convert to dictionary for storage."""
82
+ return {
83
+ "checkpoint_id": self.checkpoint_id,
84
+ "request_id": self.request_id,
85
+ "sequence": self.sequence,
86
+ "name": self.name,
87
+ "state": self.state.value,
88
+ "data": self.data,
89
+ "workflow_state": self.workflow_state,
90
+ "created_at": self.created_at.isoformat(),
91
+ "size_bytes": self.size_bytes,
92
+ }
93
+
94
+ @classmethod
95
+ def from_dict(cls, data: Dict[str, Any]) -> "Checkpoint":
96
+ """Create from dictionary."""
97
+ return cls(
98
+ checkpoint_id=data["checkpoint_id"],
99
+ request_id=data["request_id"],
100
+ sequence=data["sequence"],
101
+ name=data["name"],
102
+ state=RequestState(data["state"]),
103
+ data=data["data"],
104
+ workflow_state=data.get("workflow_state"),
105
+ created_at=datetime.fromisoformat(data["created_at"]),
106
+ size_bytes=data["size_bytes"],
107
+ )
108
+
109
+
110
+ @dataclass
111
+ class ExecutionJournal:
112
+ """Journal of all execution events."""
113
+
114
+ request_id: str
115
+ events: List[Dict[str, Any]] = field(default_factory=list)
116
+
117
+ async def record(self, event_type: str, data: Dict[str, Any]):
118
+ """Record an execution event."""
119
+ event = {
120
+ "timestamp": datetime.now(dt.UTC).isoformat(),
121
+ "type": event_type,
122
+ "data": data,
123
+ "sequence": len(self.events),
124
+ }
125
+ self.events.append(event)
126
+ logger.debug(f"Recorded event {event_type} for request {self.request_id}")
127
+
128
+ def get_events(self, event_type: Optional[str] = None) -> List[Dict[str, Any]]:
129
+ """Get events, optionally filtered by type."""
130
+ if event_type:
131
+ return [e for e in self.events if e["type"] == event_type]
132
+ return self.events
133
+
134
+
135
+ class DurableRequest:
136
+ """Durable request with automatic checkpointing and resumability."""
137
+
138
+ def __init__(
139
+ self,
140
+ request_id: Optional[str] = None,
141
+ metadata: Optional[RequestMetadata] = None,
142
+ checkpoint_manager: Optional["CheckpointManager"] = None,
143
+ ):
144
+ """Initialize durable request."""
145
+ self.id = request_id or f"req_{uuid.uuid4().hex[:12]}"
146
+ self.metadata = metadata or self._create_default_metadata()
147
+ self.state = RequestState.INITIALIZED
148
+ self.checkpoints: List[Checkpoint] = []
149
+ self.journal = ExecutionJournal(self.id)
150
+ self.checkpoint_manager = checkpoint_manager
151
+
152
+ # Execution state
153
+ self.workflow: Optional[Workflow] = None
154
+ self.workflow_id: Optional[str] = None
155
+ self.runtime: Optional[LocalRuntime] = None
156
+ self.result: Optional[Dict[str, Any]] = None
157
+ self.error: Optional[Exception] = None
158
+
159
+ # Timing
160
+ self.start_time: Optional[float] = None
161
+ self.end_time: Optional[float] = None
162
+ self.checkpoint_count = 0
163
+
164
+ # Cancellation support
165
+ self._cancel_event = asyncio.Event()
166
+
167
+ def _create_default_metadata(self) -> RequestMetadata:
168
+ """Create default metadata."""
169
+ now = datetime.now(dt.UTC)
170
+ return RequestMetadata(
171
+ request_id=self.id,
172
+ method="POST",
173
+ path="/api/workflow",
174
+ headers={},
175
+ query_params={},
176
+ body=None,
177
+ client_ip="0.0.0.0",
178
+ user_id=None,
179
+ tenant_id=None,
180
+ idempotency_key=None,
181
+ created_at=now,
182
+ updated_at=now,
183
+ )
184
+
185
+ async def execute(self) -> Dict[str, Any]:
186
+ """Execute request with automatic checkpointing."""
187
+ try:
188
+ self.start_time = time.time()
189
+ await self.journal.record(
190
+ "request_started",
191
+ {
192
+ "request_id": self.id,
193
+ "state": self.state.value,
194
+ },
195
+ )
196
+
197
+ # Validate request
198
+ await self._validate_request()
199
+
200
+ # Create workflow
201
+ await self._create_workflow()
202
+
203
+ # Execute with checkpoints
204
+ result = await self._execute_workflow()
205
+
206
+ # Mark complete
207
+ self.state = RequestState.COMPLETED
208
+ self.end_time = time.time()
209
+ self.result = result
210
+
211
+ await self.journal.record(
212
+ "request_completed",
213
+ {
214
+ "request_id": self.id,
215
+ "duration_ms": (self.end_time - self.start_time) * 1000,
216
+ "checkpoint_count": self.checkpoint_count,
217
+ },
218
+ )
219
+
220
+ return {
221
+ "request_id": self.id,
222
+ "status": "completed",
223
+ "result": result,
224
+ "duration_ms": (self.end_time - self.start_time) * 1000,
225
+ "checkpoints": self.checkpoint_count,
226
+ }
227
+
228
+ except asyncio.CancelledError:
229
+ await self._handle_cancellation()
230
+ raise
231
+
232
+ except Exception as e:
233
+ await self._handle_error(e)
234
+ raise
235
+
236
+ async def resume(self, checkpoint_id: Optional[str] = None) -> Dict[str, Any]:
237
+ """Resume execution from checkpoint."""
238
+ self.state = RequestState.RESUMING
239
+ await self.journal.record(
240
+ "request_resuming",
241
+ {
242
+ "request_id": self.id,
243
+ "checkpoint_id": checkpoint_id,
244
+ },
245
+ )
246
+
247
+ try:
248
+ # Restore from checkpoint
249
+ if checkpoint_id:
250
+ checkpoint = await self._restore_checkpoint(checkpoint_id)
251
+ else:
252
+ # Use latest checkpoint
253
+ checkpoint = await self._restore_latest_checkpoint()
254
+
255
+ if not checkpoint:
256
+ raise ValueError("No checkpoint found to resume from")
257
+
258
+ # Restore state
259
+ self.state = checkpoint.state
260
+ if checkpoint.workflow_state:
261
+ await self._restore_workflow_state(checkpoint.workflow_state)
262
+
263
+ # Continue execution
264
+ return await self.execute()
265
+
266
+ except Exception as e:
267
+ await self._handle_error(e)
268
+ raise
269
+
270
+ async def cancel(self):
271
+ """Cancel the request execution."""
272
+ self._cancel_event.set()
273
+ self.state = RequestState.CANCELLED
274
+ await self.journal.record(
275
+ "request_cancelled",
276
+ {
277
+ "request_id": self.id,
278
+ },
279
+ )
280
+
281
+ if self.workflow and self.runtime:
282
+ # TODO: Implement workflow cancellation
283
+ pass
284
+
285
+ async def checkpoint(self, name: str, data: Dict[str, Any] = None) -> str:
286
+ """Create a checkpoint."""
287
+ if self._cancel_event.is_set():
288
+ raise asyncio.CancelledError("Request was cancelled")
289
+
290
+ checkpoint = Checkpoint(
291
+ checkpoint_id=f"ckpt_{uuid.uuid4().hex[:12]}",
292
+ request_id=self.id,
293
+ sequence=self.checkpoint_count,
294
+ name=name,
295
+ state=self.state,
296
+ data=data or {},
297
+ workflow_state=(
298
+ await self._capture_workflow_state() if self.workflow else None
299
+ ),
300
+ created_at=datetime.now(dt.UTC),
301
+ size_bytes=len(json.dumps(data or {})),
302
+ )
303
+
304
+ self.checkpoints.append(checkpoint)
305
+ self.checkpoint_count += 1
306
+
307
+ # Save to checkpoint manager if available
308
+ if self.checkpoint_manager:
309
+ await self.checkpoint_manager.save_checkpoint(checkpoint)
310
+
311
+ await self.journal.record(
312
+ "checkpoint_created",
313
+ {
314
+ "checkpoint_id": checkpoint.checkpoint_id,
315
+ "name": name,
316
+ "sequence": checkpoint.sequence,
317
+ },
318
+ )
319
+
320
+ logger.info(f"Created checkpoint {name} for request {self.id}")
321
+ return checkpoint.checkpoint_id
322
+
323
+ async def _validate_request(self):
324
+ """Validate the request."""
325
+ self.state = RequestState.VALIDATED
326
+ await self.checkpoint(
327
+ "request_validated",
328
+ {
329
+ "metadata": {
330
+ "method": self.metadata.method,
331
+ "path": self.metadata.path,
332
+ "idempotency_key": self.metadata.idempotency_key,
333
+ }
334
+ },
335
+ )
336
+
337
+ async def _create_workflow(self):
338
+ """Create workflow from request."""
339
+ # This is a simplified example - in practice, this would
340
+ # parse the request and create appropriate workflow
341
+ if not self.metadata.body:
342
+ raise ValueError("Request body required for workflow creation")
343
+
344
+ workflow_config = self.metadata.body.get("workflow", {})
345
+
346
+ # Create workflow based on configuration
347
+ self.workflow = Workflow(
348
+ workflow_id=f"wf_{self.id}",
349
+ name=workflow_config.get("name", "DurableWorkflow"),
350
+ )
351
+
352
+ # TODO: Add nodes based on workflow config
353
+ # This would parse the request and build the workflow
354
+
355
+ self.workflow_id = self.workflow.workflow_id
356
+ self.state = RequestState.WORKFLOW_CREATED
357
+
358
+ await self.checkpoint(
359
+ "workflow_created",
360
+ {
361
+ "workflow_id": self.workflow_id,
362
+ "node_count": len(self.workflow.nodes),
363
+ },
364
+ )
365
+
366
+ async def _execute_workflow(self) -> Dict[str, Any]:
367
+ """Execute workflow with checkpointing."""
368
+ self.state = RequestState.EXECUTING
369
+ self.runtime = LocalRuntime()
370
+
371
+ # Execute workflow
372
+ # TODO: Implement checkpoint-aware execution
373
+ # For now, standard execution
374
+ result, run_id = await self.runtime.execute(self.workflow)
375
+
376
+ # Checkpoint final result
377
+ await self.checkpoint(
378
+ "workflow_completed",
379
+ {
380
+ "run_id": run_id,
381
+ "result": result,
382
+ },
383
+ )
384
+
385
+ return result
386
+
387
+ async def _capture_workflow_state(self) -> Dict[str, Any]:
388
+ """Capture current workflow state."""
389
+ if not self.workflow:
390
+ return {}
391
+
392
+ # TODO: Implement workflow state capture
393
+ # This would include:
394
+ # - Completed nodes
395
+ # - Node outputs
396
+ # - Workflow variables
397
+ # - Execution context
398
+
399
+ return {
400
+ "workflow_id": self.workflow_id,
401
+ "completed_nodes": [],
402
+ "node_outputs": {},
403
+ }
404
+
405
+ async def _restore_checkpoint(self, checkpoint_id: str) -> Optional[Checkpoint]:
406
+ """Restore from specific checkpoint."""
407
+ if self.checkpoint_manager:
408
+ return await self.checkpoint_manager.load_checkpoint(checkpoint_id)
409
+
410
+ # Search in memory
411
+ for ckpt in self.checkpoints:
412
+ if ckpt.checkpoint_id == checkpoint_id:
413
+ return ckpt
414
+
415
+ return None
416
+
417
+ async def _restore_latest_checkpoint(self) -> Optional[Checkpoint]:
418
+ """Restore from latest checkpoint."""
419
+ if self.checkpoint_manager:
420
+ return await self.checkpoint_manager.load_latest_checkpoint(self.id)
421
+
422
+ # Use in-memory checkpoint
423
+ return self.checkpoints[-1] if self.checkpoints else None
424
+
425
+ async def _restore_workflow_state(self, workflow_state: Dict[str, Any]):
426
+ """Restore workflow state from checkpoint."""
427
+ # TODO: Implement workflow state restoration
428
+ # This would restore:
429
+ # - Node execution state
430
+ # - Intermediate results
431
+ # - Workflow variables
432
+ pass
433
+
434
+ async def _handle_cancellation(self):
435
+ """Handle request cancellation."""
436
+ self.state = RequestState.CANCELLED
437
+ self.end_time = time.time()
438
+
439
+ await self.checkpoint(
440
+ "request_cancelled",
441
+ {
442
+ "duration_ms": (
443
+ (self.end_time - self.start_time) * 1000 if self.start_time else 0
444
+ ),
445
+ },
446
+ )
447
+
448
+ await self.journal.record(
449
+ "request_cancelled",
450
+ {
451
+ "request_id": self.id,
452
+ "checkpoints_created": self.checkpoint_count,
453
+ },
454
+ )
455
+
456
+ async def _handle_error(self, error: Exception):
457
+ """Handle execution error."""
458
+ self.state = RequestState.FAILED
459
+ self.end_time = time.time()
460
+ self.error = error
461
+
462
+ await self.checkpoint(
463
+ "request_failed",
464
+ {
465
+ "error": str(error),
466
+ "error_type": type(error).__name__,
467
+ },
468
+ )
469
+
470
+ await self.journal.record(
471
+ "request_failed",
472
+ {
473
+ "request_id": self.id,
474
+ "error": str(error),
475
+ "error_type": type(error).__name__,
476
+ "duration_ms": (
477
+ (self.end_time - self.start_time) * 1000 if self.start_time else 0
478
+ ),
479
+ },
480
+ )
481
+
482
+ logger.error(f"Request {self.id} failed: {error}")
483
+
484
+ def get_status(self) -> Dict[str, Any]:
485
+ """Get current request status."""
486
+ return {
487
+ "request_id": self.id,
488
+ "state": self.state.value,
489
+ "checkpoints": self.checkpoint_count,
490
+ "events": len(self.journal.events),
491
+ "duration_ms": (
492
+ (self.end_time - self.start_time) * 1000
493
+ if self.start_time and self.end_time
494
+ else None
495
+ ),
496
+ "result": self.result,
497
+ "error": str(self.error) if self.error else None,
498
+ }