kailash 0.6.6__py3-none-any.whl → 0.8.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 (82) hide show
  1. kailash/__init__.py +35 -5
  2. kailash/access_control.py +64 -46
  3. kailash/adapters/__init__.py +5 -0
  4. kailash/adapters/mcp_platform_adapter.py +273 -0
  5. kailash/api/workflow_api.py +34 -3
  6. kailash/channels/__init__.py +21 -0
  7. kailash/channels/api_channel.py +409 -0
  8. kailash/channels/base.py +271 -0
  9. kailash/channels/cli_channel.py +661 -0
  10. kailash/channels/event_router.py +496 -0
  11. kailash/channels/mcp_channel.py +648 -0
  12. kailash/channels/session.py +423 -0
  13. kailash/mcp_server/discovery.py +57 -18
  14. kailash/middleware/communication/api_gateway.py +23 -3
  15. kailash/middleware/communication/realtime.py +83 -0
  16. kailash/middleware/core/agent_ui.py +1 -1
  17. kailash/middleware/gateway/storage_backends.py +393 -0
  18. kailash/middleware/mcp/enhanced_server.py +22 -16
  19. kailash/nexus/__init__.py +21 -0
  20. kailash/nexus/cli/__init__.py +5 -0
  21. kailash/nexus/cli/__main__.py +6 -0
  22. kailash/nexus/cli/main.py +176 -0
  23. kailash/nexus/factory.py +413 -0
  24. kailash/nexus/gateway.py +545 -0
  25. kailash/nodes/__init__.py +8 -5
  26. kailash/nodes/ai/iterative_llm_agent.py +988 -17
  27. kailash/nodes/ai/llm_agent.py +29 -9
  28. kailash/nodes/api/__init__.py +2 -2
  29. kailash/nodes/api/monitoring.py +1 -1
  30. kailash/nodes/base.py +29 -5
  31. kailash/nodes/base_async.py +54 -14
  32. kailash/nodes/code/async_python.py +1 -1
  33. kailash/nodes/code/python.py +50 -6
  34. kailash/nodes/data/async_sql.py +90 -0
  35. kailash/nodes/data/bulk_operations.py +939 -0
  36. kailash/nodes/data/query_builder.py +373 -0
  37. kailash/nodes/data/query_cache.py +512 -0
  38. kailash/nodes/monitoring/__init__.py +10 -0
  39. kailash/nodes/monitoring/deadlock_detector.py +964 -0
  40. kailash/nodes/monitoring/performance_anomaly.py +1078 -0
  41. kailash/nodes/monitoring/race_condition_detector.py +1151 -0
  42. kailash/nodes/monitoring/transaction_metrics.py +790 -0
  43. kailash/nodes/monitoring/transaction_monitor.py +931 -0
  44. kailash/nodes/security/behavior_analysis.py +414 -0
  45. kailash/nodes/system/__init__.py +17 -0
  46. kailash/nodes/system/command_parser.py +820 -0
  47. kailash/nodes/transaction/__init__.py +48 -0
  48. kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
  49. kailash/nodes/transaction/saga_coordinator.py +652 -0
  50. kailash/nodes/transaction/saga_state_storage.py +411 -0
  51. kailash/nodes/transaction/saga_step.py +467 -0
  52. kailash/nodes/transaction/transaction_context.py +756 -0
  53. kailash/nodes/transaction/two_phase_commit.py +978 -0
  54. kailash/nodes/transform/processors.py +17 -1
  55. kailash/nodes/validation/__init__.py +21 -0
  56. kailash/nodes/validation/test_executor.py +532 -0
  57. kailash/nodes/validation/validation_nodes.py +447 -0
  58. kailash/resources/factory.py +1 -1
  59. kailash/runtime/access_controlled.py +9 -7
  60. kailash/runtime/async_local.py +84 -21
  61. kailash/runtime/local.py +21 -2
  62. kailash/runtime/parameter_injector.py +187 -31
  63. kailash/runtime/runner.py +6 -4
  64. kailash/runtime/testing.py +1 -1
  65. kailash/security.py +22 -3
  66. kailash/servers/__init__.py +32 -0
  67. kailash/servers/durable_workflow_server.py +430 -0
  68. kailash/servers/enterprise_workflow_server.py +522 -0
  69. kailash/servers/gateway.py +183 -0
  70. kailash/servers/workflow_server.py +293 -0
  71. kailash/utils/data_validation.py +192 -0
  72. kailash/workflow/builder.py +382 -15
  73. kailash/workflow/cyclic_runner.py +102 -10
  74. kailash/workflow/validation.py +144 -8
  75. kailash/workflow/visualization.py +99 -27
  76. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/METADATA +3 -2
  77. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/RECORD +81 -40
  78. kailash/workflow/builder_improvements.py +0 -207
  79. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/WHEEL +0 -0
  80. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/entry_points.txt +0 -0
  81. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/licenses/LICENSE +0 -0
  82. {kailash-0.6.6.dist-info → kailash-0.8.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,467 @@
1
+ """Saga Step Node for executing individual steps in a distributed transaction.
2
+
3
+ Each saga step represents a local transaction that can be compensated if needed.
4
+ """
5
+
6
+ import json
7
+ import logging
8
+ import time
9
+ import uuid
10
+ from datetime import UTC, datetime
11
+ from typing import Any, Callable, Dict, Optional
12
+
13
+ from kailash.nodes.base import NodeParameter, register_node
14
+ from kailash.nodes.base_async import AsyncNode
15
+ from kailash.sdk_exceptions import NodeExecutionError
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ @register_node()
21
+ class SagaStepNode(AsyncNode):
22
+ """Executes individual steps within a Saga transaction.
23
+
24
+ Each SagaStepNode represents a single, compensatable unit of work within
25
+ a distributed transaction. It encapsulates both the forward action and
26
+ its compensating action.
27
+
28
+ Features:
29
+ - Idempotent execution
30
+ - Built-in compensation logic
31
+ - State tracking
32
+ - Retry support
33
+ - Monitoring integration
34
+
35
+ Examples:
36
+ >>> # Execute a saga step
37
+ >>> step = SagaStepNode(step_name="process_payment")
38
+ >>> result = await step.execute(
39
+ ... operation="execute",
40
+ ... execution_id="exec_123",
41
+ ... saga_context={"order_id": "order_456"},
42
+ ... data={"amount": 100.0, "currency": "USD"}
43
+ ... )
44
+
45
+ >>> # Compensate if needed
46
+ >>> result = await step.execute(
47
+ ... operation="compensate",
48
+ ... execution_id="exec_123"
49
+ ... )
50
+ """
51
+
52
+ def __init__(self, **kwargs):
53
+ # Set node-specific attributes before calling parent
54
+ self.step_name = kwargs.pop("step_name", "saga_step")
55
+ self.idempotent = kwargs.pop("idempotent", True)
56
+ self.retry_on_failure = kwargs.pop("retry_on_failure", True)
57
+ self.max_retries = kwargs.pop("max_retries", 3)
58
+ self.retry_delay = kwargs.pop("retry_delay", 1.0)
59
+ self.timeout = kwargs.pop("timeout", 300.0) # 5 minutes default
60
+ self.enable_monitoring = kwargs.pop("enable_monitoring", True)
61
+
62
+ # Compensation settings
63
+ self.compensation_timeout = kwargs.pop(
64
+ "compensation_timeout", 600.0
65
+ ) # 10 minutes
66
+ self.compensation_retries = kwargs.pop("compensation_retries", 5)
67
+
68
+ # State tracking
69
+ self.execution_id: Optional[str] = None
70
+ self.execution_state: Dict[str, Any] = {}
71
+ self.compensation_state: Dict[str, Any] = {}
72
+
73
+ # Custom action handlers (can be overridden by subclasses)
74
+ self.forward_action: Optional[Callable] = None
75
+ self.compensation_action: Optional[Callable] = None
76
+
77
+ super().__init__(**kwargs)
78
+
79
+ def get_parameters(self) -> Dict[str, NodeParameter]:
80
+ """Define the parameters for the Saga Step node."""
81
+ return {
82
+ "operation": NodeParameter(
83
+ name="operation",
84
+ type=str,
85
+ default="execute",
86
+ description="Operation to perform",
87
+ ),
88
+ "execution_id": NodeParameter(
89
+ name="execution_id",
90
+ type=str,
91
+ required=False,
92
+ description="Unique execution identifier",
93
+ ),
94
+ "saga_context": NodeParameter(
95
+ name="saga_context",
96
+ type=dict,
97
+ default={},
98
+ description="Saga context data",
99
+ ),
100
+ "action_type": NodeParameter(
101
+ name="action_type",
102
+ type=str,
103
+ default="process",
104
+ description="Type of action to perform",
105
+ ),
106
+ "data": NodeParameter(
107
+ name="data",
108
+ type=dict,
109
+ default={},
110
+ description="Data to process",
111
+ ),
112
+ "required_inputs": NodeParameter(
113
+ name="required_inputs",
114
+ type=list,
115
+ default=[],
116
+ description="Required inputs for validation",
117
+ ),
118
+ }
119
+
120
+ def execute(self, **runtime_inputs) -> Dict[str, Any]:
121
+ """Execute the saga step based on the requested operation."""
122
+ # For sync compatibility with LocalRuntime, we don't make this async
123
+ # The AsyncNode base class handles running async_run in a sync context
124
+ operation = runtime_inputs.get("operation", "execute")
125
+
126
+ operations = {
127
+ "execute": self._execute_forward,
128
+ "compensate": self._execute_compensation,
129
+ "get_status": self._get_status,
130
+ "validate": self._validate_preconditions,
131
+ }
132
+
133
+ if operation not in operations:
134
+ raise NodeExecutionError(f"Unknown operation: {operation}")
135
+
136
+ try:
137
+ return operations[operation](runtime_inputs)
138
+ except Exception as e:
139
+ logger.error(f"Saga step error in {self.step_name}: {e}")
140
+ return {
141
+ "status": "error",
142
+ "step_name": self.step_name,
143
+ "error": str(e),
144
+ "operation": operation,
145
+ }
146
+
147
+ def _execute_forward(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
148
+ """Execute the forward action of the saga step."""
149
+ self.execution_id = inputs.get("execution_id", str(uuid.uuid4()))
150
+ saga_context = inputs.get("saga_context", {})
151
+
152
+ # Check idempotency
153
+ if self.idempotent and self._check_already_executed():
154
+ logger.info(
155
+ f"Step {self.step_name} already executed for {self.execution_id}"
156
+ )
157
+ return self._get_cached_result()
158
+
159
+ # Validate preconditions
160
+ validation_result = self._validate_preconditions(inputs)
161
+ if validation_result.get("status") != "valid":
162
+ return validation_result
163
+
164
+ # Execute with retries
165
+ attempt = 0
166
+ last_error = None
167
+
168
+ while attempt < self.max_retries:
169
+ try:
170
+ # Log execution start
171
+ self._log_execution_start()
172
+
173
+ # Execute the actual business logic
174
+ if self.forward_action:
175
+ result = self.forward_action(inputs, saga_context)
176
+ else:
177
+ result = self._default_forward_action(inputs, saga_context)
178
+
179
+ # Store result for idempotency
180
+ self.execution_state = {
181
+ "execution_id": self.execution_id,
182
+ "result": result,
183
+ "timestamp": datetime.now(UTC).isoformat(),
184
+ "attempts": attempt + 1,
185
+ }
186
+
187
+ self._log_execution_complete(result)
188
+
189
+ return {
190
+ "status": "success",
191
+ "step_name": self.step_name,
192
+ "execution_id": self.execution_id,
193
+ "data": result,
194
+ "attempts": attempt + 1,
195
+ }
196
+
197
+ except Exception as e:
198
+ last_error = e
199
+ attempt += 1
200
+ logger.warning(
201
+ f"Step {self.step_name} failed on attempt {attempt}: {e}"
202
+ )
203
+
204
+ if attempt < self.max_retries:
205
+ time.sleep(self.retry_delay * attempt) # Exponential backoff
206
+
207
+ # All retries exhausted
208
+ self._log_execution_failed(str(last_error))
209
+
210
+ return {
211
+ "status": "failed",
212
+ "step_name": self.step_name,
213
+ "execution_id": self.execution_id,
214
+ "error": str(last_error),
215
+ "attempts": attempt,
216
+ }
217
+
218
+ def _execute_compensation(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
219
+ """Execute the compensation action of the saga step."""
220
+ self.execution_id = inputs.get("execution_id", self.execution_id)
221
+ saga_context = inputs.get("saga_context", {})
222
+
223
+ # Check if compensation is needed
224
+ if not self.execution_state:
225
+ logger.info(
226
+ f"No forward execution found for {self.step_name}, skipping compensation"
227
+ )
228
+ return {
229
+ "status": "skipped",
230
+ "step_name": self.step_name,
231
+ "message": "No forward execution to compensate",
232
+ }
233
+
234
+ # Check if already compensated
235
+ if self._check_already_compensated():
236
+ logger.info(f"Step {self.step_name} already compensated")
237
+ return {
238
+ "status": "already_compensated",
239
+ "step_name": self.step_name,
240
+ "execution_id": self.execution_id,
241
+ }
242
+
243
+ # Execute compensation with retries
244
+ attempt = 0
245
+ last_error = None
246
+
247
+ while attempt < self.compensation_retries:
248
+ try:
249
+ # Log compensation start
250
+ self._log_compensation_start()
251
+
252
+ # Execute the compensation logic
253
+ if self.compensation_action:
254
+ result = self.compensation_action(
255
+ inputs, saga_context, self.execution_state
256
+ )
257
+ else:
258
+ result = self._default_compensation_action(
259
+ inputs, saga_context, self.execution_state
260
+ )
261
+
262
+ # Store compensation result
263
+ self.compensation_state = {
264
+ "execution_id": self.execution_id,
265
+ "result": result,
266
+ "timestamp": datetime.now(UTC).isoformat(),
267
+ "attempts": attempt + 1,
268
+ }
269
+
270
+ self._log_compensation_complete(result)
271
+
272
+ return {
273
+ "status": "compensated",
274
+ "step_name": self.step_name,
275
+ "execution_id": self.execution_id,
276
+ "compensation_result": result,
277
+ "attempts": attempt + 1,
278
+ }
279
+
280
+ except Exception as e:
281
+ last_error = e
282
+ attempt += 1
283
+ logger.warning(
284
+ f"Compensation for {self.step_name} failed on attempt {attempt}: {e}"
285
+ )
286
+
287
+ if attempt < self.compensation_retries:
288
+ time.sleep(self.retry_delay * attempt)
289
+
290
+ # Compensation failed
291
+ self._log_compensation_failed(str(last_error))
292
+
293
+ return {
294
+ "status": "compensation_failed",
295
+ "step_name": self.step_name,
296
+ "execution_id": self.execution_id,
297
+ "error": str(last_error),
298
+ "attempts": attempt,
299
+ }
300
+
301
+ def _validate_preconditions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
302
+ """Validate preconditions before executing the step."""
303
+ # Override in subclasses for specific validation
304
+ required_inputs = inputs.get("required_inputs", [])
305
+ saga_context = inputs.get("saga_context", {})
306
+
307
+ missing_inputs = []
308
+ for required in required_inputs:
309
+ if required not in saga_context:
310
+ missing_inputs.append(required)
311
+
312
+ if missing_inputs:
313
+ return {
314
+ "status": "invalid",
315
+ "step_name": self.step_name,
316
+ "missing_inputs": missing_inputs,
317
+ "message": f"Missing required inputs: {missing_inputs}",
318
+ }
319
+
320
+ return {
321
+ "status": "valid",
322
+ "step_name": self.step_name,
323
+ "message": "All preconditions satisfied",
324
+ }
325
+
326
+ def _get_status(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
327
+ """Get the current status of the saga step."""
328
+ return {
329
+ "status": "success",
330
+ "step_name": self.step_name,
331
+ "execution_state": self.execution_state,
332
+ "compensation_state": self.compensation_state,
333
+ "idempotent": self.idempotent,
334
+ "retry_settings": {
335
+ "max_retries": self.max_retries,
336
+ "retry_delay": self.retry_delay,
337
+ "compensation_retries": self.compensation_retries,
338
+ },
339
+ }
340
+
341
+ def _default_forward_action(
342
+ self, inputs: Dict[str, Any], saga_context: Dict[str, Any]
343
+ ) -> Dict[str, Any]:
344
+ """Default forward action implementation."""
345
+ # Override in subclasses or provide custom forward_action
346
+ action_type = inputs.get("action_type", "process")
347
+ data = inputs.get("data", {})
348
+
349
+ # Simulate some processing
350
+ result = {
351
+ "action": action_type,
352
+ "processed_data": data,
353
+ "timestamp": datetime.now(UTC).isoformat(),
354
+ "step": self.step_name,
355
+ }
356
+
357
+ return result
358
+
359
+ def _default_compensation_action(
360
+ self,
361
+ inputs: Dict[str, Any],
362
+ saga_context: Dict[str, Any],
363
+ execution_state: Dict[str, Any],
364
+ ) -> Dict[str, Any]:
365
+ """Default compensation action implementation."""
366
+ # Override in subclasses or provide custom compensation_action
367
+ result = {
368
+ "action": "compensate",
369
+ "compensated_execution": execution_state.get("execution_id"),
370
+ "timestamp": datetime.now(UTC).isoformat(),
371
+ "step": self.step_name,
372
+ }
373
+
374
+ return result
375
+
376
+ def _check_already_executed(self) -> bool:
377
+ """Check if this step has already been executed."""
378
+ return bool(self.execution_state)
379
+
380
+ def _check_already_compensated(self) -> bool:
381
+ """Check if this step has already been compensated."""
382
+ return bool(self.compensation_state)
383
+
384
+ def _get_cached_result(self) -> Dict[str, Any]:
385
+ """Get the cached result from a previous execution."""
386
+ return {
387
+ "status": "success",
388
+ "step_name": self.step_name,
389
+ "execution_id": self.execution_id,
390
+ "data": self.execution_state.get("result", {}),
391
+ "cached": True,
392
+ "cached_at": self.execution_state.get("timestamp"),
393
+ }
394
+
395
+ def _log_execution_start(self):
396
+ """Log the start of step execution."""
397
+ if self.enable_monitoring:
398
+ logger.info(
399
+ f"Starting execution of saga step: {self.step_name}",
400
+ extra={
401
+ "step_name": self.step_name,
402
+ "execution_id": self.execution_id,
403
+ "event": "saga_step_started",
404
+ },
405
+ )
406
+
407
+ def _log_execution_complete(self, result: Any):
408
+ """Log successful completion of step execution."""
409
+ if self.enable_monitoring:
410
+ logger.info(
411
+ f"Completed execution of saga step: {self.step_name}",
412
+ extra={
413
+ "step_name": self.step_name,
414
+ "execution_id": self.execution_id,
415
+ "event": "saga_step_completed",
416
+ },
417
+ )
418
+
419
+ def _log_execution_failed(self, error: str):
420
+ """Log failure of step execution."""
421
+ if self.enable_monitoring:
422
+ logger.error(
423
+ f"Failed execution of saga step: {self.step_name}",
424
+ extra={
425
+ "step_name": self.step_name,
426
+ "execution_id": self.execution_id,
427
+ "error": error,
428
+ "event": "saga_step_failed",
429
+ },
430
+ )
431
+
432
+ def _log_compensation_start(self):
433
+ """Log the start of compensation."""
434
+ if self.enable_monitoring:
435
+ logger.info(
436
+ f"Starting compensation for saga step: {self.step_name}",
437
+ extra={
438
+ "step_name": self.step_name,
439
+ "execution_id": self.execution_id,
440
+ "event": "saga_compensation_started",
441
+ },
442
+ )
443
+
444
+ def _log_compensation_complete(self, result: Any):
445
+ """Log successful completion of compensation."""
446
+ if self.enable_monitoring:
447
+ logger.info(
448
+ f"Completed compensation for saga step: {self.step_name}",
449
+ extra={
450
+ "step_name": self.step_name,
451
+ "execution_id": self.execution_id,
452
+ "event": "saga_compensation_completed",
453
+ },
454
+ )
455
+
456
+ def _log_compensation_failed(self, error: str):
457
+ """Log failure of compensation."""
458
+ if self.enable_monitoring:
459
+ logger.error(
460
+ f"Failed compensation for saga step: {self.step_name}",
461
+ extra={
462
+ "step_name": self.step_name,
463
+ "execution_id": self.execution_id,
464
+ "error": error,
465
+ "event": "saga_compensation_failed",
466
+ },
467
+ )