kailash 0.6.3__py3-none-any.whl → 0.6.4__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 (120) hide show
  1. kailash/__init__.py +3 -3
  2. kailash/api/custom_nodes_secure.py +3 -3
  3. kailash/api/gateway.py +1 -1
  4. kailash/api/studio.py +2 -3
  5. kailash/api/workflow_api.py +3 -4
  6. kailash/core/resilience/bulkhead.py +460 -0
  7. kailash/core/resilience/circuit_breaker.py +92 -10
  8. kailash/edge/discovery.py +86 -0
  9. kailash/mcp_server/__init__.py +309 -33
  10. kailash/mcp_server/advanced_features.py +1022 -0
  11. kailash/mcp_server/ai_registry_server.py +27 -2
  12. kailash/mcp_server/auth.py +789 -0
  13. kailash/mcp_server/client.py +645 -378
  14. kailash/mcp_server/discovery.py +1593 -0
  15. kailash/mcp_server/errors.py +673 -0
  16. kailash/mcp_server/oauth.py +1727 -0
  17. kailash/mcp_server/protocol.py +1126 -0
  18. kailash/mcp_server/registry_integration.py +587 -0
  19. kailash/mcp_server/server.py +1213 -98
  20. kailash/mcp_server/transports.py +1169 -0
  21. kailash/mcp_server/utils/__init__.py +6 -1
  22. kailash/mcp_server/utils/cache.py +250 -7
  23. kailash/middleware/auth/auth_manager.py +3 -3
  24. kailash/middleware/communication/api_gateway.py +2 -9
  25. kailash/middleware/communication/realtime.py +1 -1
  26. kailash/middleware/mcp/enhanced_server.py +1 -1
  27. kailash/nodes/__init__.py +2 -0
  28. kailash/nodes/admin/audit_log.py +6 -6
  29. kailash/nodes/admin/permission_check.py +8 -8
  30. kailash/nodes/admin/role_management.py +32 -28
  31. kailash/nodes/admin/schema.sql +6 -1
  32. kailash/nodes/admin/schema_manager.py +13 -13
  33. kailash/nodes/admin/security_event.py +16 -20
  34. kailash/nodes/admin/tenant_isolation.py +3 -3
  35. kailash/nodes/admin/transaction_utils.py +3 -3
  36. kailash/nodes/admin/user_management.py +21 -22
  37. kailash/nodes/ai/a2a.py +11 -11
  38. kailash/nodes/ai/ai_providers.py +9 -12
  39. kailash/nodes/ai/embedding_generator.py +13 -14
  40. kailash/nodes/ai/intelligent_agent_orchestrator.py +19 -19
  41. kailash/nodes/ai/iterative_llm_agent.py +2 -2
  42. kailash/nodes/ai/llm_agent.py +210 -33
  43. kailash/nodes/ai/self_organizing.py +2 -2
  44. kailash/nodes/alerts/discord.py +4 -4
  45. kailash/nodes/api/graphql.py +6 -6
  46. kailash/nodes/api/http.py +12 -17
  47. kailash/nodes/api/rate_limiting.py +4 -4
  48. kailash/nodes/api/rest.py +15 -15
  49. kailash/nodes/auth/mfa.py +3 -4
  50. kailash/nodes/auth/risk_assessment.py +2 -2
  51. kailash/nodes/auth/session_management.py +5 -5
  52. kailash/nodes/auth/sso.py +143 -0
  53. kailash/nodes/base.py +6 -2
  54. kailash/nodes/base_async.py +16 -2
  55. kailash/nodes/base_with_acl.py +2 -2
  56. kailash/nodes/cache/__init__.py +9 -0
  57. kailash/nodes/cache/cache.py +1172 -0
  58. kailash/nodes/cache/cache_invalidation.py +870 -0
  59. kailash/nodes/cache/redis_pool_manager.py +595 -0
  60. kailash/nodes/code/async_python.py +2 -1
  61. kailash/nodes/code/python.py +196 -35
  62. kailash/nodes/compliance/data_retention.py +6 -6
  63. kailash/nodes/compliance/gdpr.py +5 -5
  64. kailash/nodes/data/__init__.py +10 -0
  65. kailash/nodes/data/optimistic_locking.py +906 -0
  66. kailash/nodes/data/readers.py +8 -8
  67. kailash/nodes/data/redis.py +349 -0
  68. kailash/nodes/data/sql.py +314 -3
  69. kailash/nodes/data/streaming.py +21 -0
  70. kailash/nodes/enterprise/__init__.py +8 -0
  71. kailash/nodes/enterprise/audit_logger.py +285 -0
  72. kailash/nodes/enterprise/batch_processor.py +22 -3
  73. kailash/nodes/enterprise/data_lineage.py +1 -1
  74. kailash/nodes/enterprise/mcp_executor.py +205 -0
  75. kailash/nodes/enterprise/service_discovery.py +150 -0
  76. kailash/nodes/enterprise/tenant_assignment.py +108 -0
  77. kailash/nodes/logic/async_operations.py +2 -2
  78. kailash/nodes/logic/convergence.py +1 -1
  79. kailash/nodes/logic/operations.py +1 -1
  80. kailash/nodes/monitoring/__init__.py +11 -1
  81. kailash/nodes/monitoring/health_check.py +456 -0
  82. kailash/nodes/monitoring/log_processor.py +817 -0
  83. kailash/nodes/monitoring/metrics_collector.py +627 -0
  84. kailash/nodes/monitoring/performance_benchmark.py +137 -11
  85. kailash/nodes/rag/advanced.py +7 -7
  86. kailash/nodes/rag/agentic.py +49 -2
  87. kailash/nodes/rag/conversational.py +3 -3
  88. kailash/nodes/rag/evaluation.py +3 -3
  89. kailash/nodes/rag/federated.py +3 -3
  90. kailash/nodes/rag/graph.py +3 -3
  91. kailash/nodes/rag/multimodal.py +3 -3
  92. kailash/nodes/rag/optimized.py +5 -5
  93. kailash/nodes/rag/privacy.py +3 -3
  94. kailash/nodes/rag/query_processing.py +6 -6
  95. kailash/nodes/rag/realtime.py +1 -1
  96. kailash/nodes/rag/registry.py +2 -6
  97. kailash/nodes/rag/router.py +1 -1
  98. kailash/nodes/rag/similarity.py +7 -7
  99. kailash/nodes/rag/strategies.py +4 -4
  100. kailash/nodes/security/abac_evaluator.py +6 -6
  101. kailash/nodes/security/behavior_analysis.py +5 -6
  102. kailash/nodes/security/credential_manager.py +1 -1
  103. kailash/nodes/security/rotating_credentials.py +11 -11
  104. kailash/nodes/security/threat_detection.py +8 -8
  105. kailash/nodes/testing/credential_testing.py +2 -2
  106. kailash/nodes/transform/processors.py +5 -5
  107. kailash/runtime/local.py +162 -14
  108. kailash/runtime/parameter_injection.py +425 -0
  109. kailash/runtime/parameter_injector.py +657 -0
  110. kailash/runtime/testing.py +2 -2
  111. kailash/testing/fixtures.py +2 -2
  112. kailash/workflow/builder.py +99 -18
  113. kailash/workflow/builder_improvements.py +207 -0
  114. kailash/workflow/input_handling.py +170 -0
  115. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/METADATA +22 -9
  116. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/RECORD +120 -94
  117. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/WHEEL +0 -0
  118. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/entry_points.txt +0 -0
  119. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/licenses/LICENSE +0 -0
  120. {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/top_level.txt +0 -0
kailash/runtime/local.py CHANGED
@@ -40,8 +40,8 @@ from datetime import UTC, datetime
40
40
  from typing import Any, Optional
41
41
 
42
42
  import networkx as nx
43
-
44
43
  from kailash.nodes import Node
44
+ from kailash.runtime.parameter_injector import WorkflowParameterInjector
45
45
  from kailash.sdk_exceptions import (
46
46
  RuntimeExecutionError,
47
47
  WorkflowExecutionError,
@@ -135,7 +135,7 @@ class LocalRuntime:
135
135
  self,
136
136
  workflow: Workflow,
137
137
  task_manager: TaskManager | None = None,
138
- parameters: dict[str, dict[str, Any]] | None = None,
138
+ parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
139
139
  ) -> tuple[dict[str, Any], str | None]:
140
140
  """Execute a workflow with unified enterprise capabilities.
141
141
 
@@ -172,7 +172,7 @@ class LocalRuntime:
172
172
  self,
173
173
  workflow: Workflow,
174
174
  task_manager: TaskManager | None = None,
175
- parameters: dict[str, dict[str, Any]] | None = None,
175
+ parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
176
176
  ) -> tuple[dict[str, Any], str | None]:
177
177
  """Execute a workflow asynchronously (for AsyncLocalRuntime compatibility).
178
178
 
@@ -197,7 +197,7 @@ class LocalRuntime:
197
197
  self,
198
198
  workflow: Workflow,
199
199
  task_manager: TaskManager | None = None,
200
- parameters: dict[str, dict[str, Any]] | None = None,
200
+ parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
201
201
  ) -> tuple[dict[str, Any], str | None]:
202
202
  """Execute workflow synchronously when already in an event loop.
203
203
 
@@ -257,7 +257,7 @@ class LocalRuntime:
257
257
  self,
258
258
  workflow: Workflow,
259
259
  task_manager: TaskManager | None = None,
260
- parameters: dict[str, dict[str, Any]] | None = None,
260
+ parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
261
261
  ) -> tuple[dict[str, Any], str | None]:
262
262
  """Core async execution implementation with enterprise features.
263
263
 
@@ -292,8 +292,13 @@ class LocalRuntime:
292
292
  if self.enable_security and self.user_context:
293
293
  self._check_workflow_access(workflow)
294
294
 
295
+ # Transform workflow-level parameters if needed
296
+ processed_parameters = self._process_workflow_parameters(
297
+ workflow, parameters
298
+ )
299
+
295
300
  # Validate workflow with runtime parameters (Session 061)
296
- workflow.validate(runtime_parameters=parameters)
301
+ workflow.validate(runtime_parameters=processed_parameters)
297
302
 
298
303
  # Enterprise Audit: Log workflow execution start
299
304
  if self.enable_audit:
@@ -302,7 +307,7 @@ class LocalRuntime:
302
307
  {
303
308
  "workflow_id": workflow.workflow_id,
304
309
  "user_context": self._serialize_user_context(),
305
- "parameters": parameters,
310
+ "parameters": processed_parameters,
306
311
  },
307
312
  )
308
313
 
@@ -315,7 +320,7 @@ class LocalRuntime:
315
320
  run_id = task_manager.create_run(
316
321
  workflow_name=workflow.name,
317
322
  metadata={
318
- "parameters": parameters,
323
+ "parameters": processed_parameters,
319
324
  "debug": self.debug,
320
325
  "runtime": "unified_enterprise",
321
326
  "enterprise_features": self._execution_context,
@@ -335,7 +340,7 @@ class LocalRuntime:
335
340
  try:
336
341
  # Pass run_id to cyclic executor if available
337
342
  cyclic_results, cyclic_run_id = self.cyclic_executor.execute(
338
- workflow, parameters, task_manager, run_id
343
+ workflow, processed_parameters, task_manager, run_id
339
344
  )
340
345
  results = cyclic_results
341
346
  # Update run_id if task manager is being used
@@ -354,7 +359,7 @@ class LocalRuntime:
354
359
  workflow=workflow,
355
360
  task_manager=task_manager,
356
361
  run_id=run_id,
357
- parameters=parameters or {},
362
+ parameters=processed_parameters or {},
358
363
  )
359
364
 
360
365
  # Enterprise Audit: Log successful completion
@@ -543,6 +548,14 @@ class LocalRuntime:
543
548
  # Update node config with parameters (Session 061: direct config update)
544
549
  {**node_instance.config, **parameters.get(node_id, {})}
545
550
  node_instance.config.update(parameters.get(node_id, {}))
551
+
552
+ # ENTERPRISE PARAMETER INJECTION FIX: Injected parameters should override connection inputs
553
+ # This ensures workflow parameters take precedence over connection inputs for the same parameter names
554
+ injected_params = parameters.get(node_id, {})
555
+ if injected_params:
556
+ inputs.update(injected_params)
557
+ if self.debug:
558
+ self.logger.debug(f"Applied parameter injections for {node_id}: {list(injected_params.keys())}")
546
559
 
547
560
  if self.debug:
548
561
  self.logger.debug(f"Node {node_id} inputs: {inputs}")
@@ -856,10 +869,7 @@ class LocalRuntime:
856
869
 
857
870
  try:
858
871
  # Use existing AccessControlManager pattern
859
- from kailash.access_control import (
860
- WorkflowPermission,
861
- get_access_control_manager,
862
- )
872
+ from kailash.access_control import WorkflowPermission, get_access_control_manager
863
873
 
864
874
  if self._access_control_manager is None:
865
875
  self._access_control_manager = get_access_control_manager()
@@ -961,3 +971,141 @@ class LocalRuntime:
961
971
  except Exception as e:
962
972
  self.logger.warning(f"Failed to serialize user context: {e}")
963
973
  return {"user_context": str(self.user_context)}
974
+
975
+ def _process_workflow_parameters(
976
+ self,
977
+ workflow: Workflow,
978
+ parameters: dict[str, dict[str, Any]] | dict[str, Any] | None = None,
979
+ ) -> dict[str, dict[str, Any]] | None:
980
+ """Process workflow parameters to handle both formats intelligently.
981
+
982
+ This method detects whether parameters are in workflow-level format
983
+ (flat dictionary) or node-specific format (nested dictionary) and
984
+ transforms them appropriately for execution.
985
+
986
+ ENTERPRISE ENHANCEMENT: Handles mixed format parameters where both
987
+ node-specific and workflow-level parameters are present in the same
988
+ parameter dictionary - critical for enterprise production workflows.
989
+
990
+ Args:
991
+ workflow: The workflow being executed
992
+ parameters: Either workflow-level, node-specific, or MIXED format parameters
993
+
994
+ Returns:
995
+ Node-specific parameters ready for execution with workflow-level
996
+ parameters properly injected
997
+ """
998
+ if not parameters:
999
+ return None
1000
+
1001
+ # ENTERPRISE FIX: Handle mixed format parameters
1002
+ # Extract node-specific and workflow-level parameters separately
1003
+ node_specific_params, workflow_level_params = self._separate_parameter_formats(
1004
+ parameters, workflow
1005
+ )
1006
+
1007
+ # Start with node-specific parameters
1008
+ result = node_specific_params.copy() if node_specific_params else {}
1009
+
1010
+ # If we have workflow-level parameters, inject them
1011
+ if workflow_level_params:
1012
+ injector = WorkflowParameterInjector(workflow, debug=self.debug)
1013
+
1014
+ # Transform workflow parameters to node-specific format
1015
+ injected_params = injector.transform_workflow_parameters(
1016
+ workflow_level_params
1017
+ )
1018
+
1019
+ # Merge injected parameters with existing node-specific parameters
1020
+ # IMPORTANT: Node-specific parameters take precedence over workflow-level
1021
+ for node_id, node_params in injected_params.items():
1022
+ if node_id not in result:
1023
+ result[node_id] = {}
1024
+ # First set workflow-level parameters, then override with node-specific
1025
+ for param_name, param_value in node_params.items():
1026
+ if param_name not in result[node_id]: # Only if not already set
1027
+ result[node_id][param_name] = param_value
1028
+
1029
+ # Validate the transformation
1030
+ warnings = injector.validate_parameters(workflow_level_params)
1031
+ if warnings and self.debug:
1032
+ for warning in warnings:
1033
+ self.logger.warning(f"Parameter validation: {warning}")
1034
+
1035
+ return result if result else None
1036
+
1037
+ def _separate_parameter_formats(
1038
+ self, parameters: dict[str, Any], workflow: Workflow
1039
+ ) -> tuple[dict[str, dict[str, Any]], dict[str, Any]]:
1040
+ """Separate mixed format parameters into node-specific and workflow-level.
1041
+
1042
+ ENTERPRISE CAPABILITY: Intelligently separates complex enterprise parameter
1043
+ patterns where both node-specific and workflow-level parameters coexist.
1044
+
1045
+ Args:
1046
+ parameters: Mixed format parameters
1047
+ workflow: The workflow being executed
1048
+
1049
+ Returns:
1050
+ Tuple of (node_specific_params, workflow_level_params)
1051
+ """
1052
+ node_specific_params = {}
1053
+ workflow_level_params = {}
1054
+
1055
+ # Get node IDs for classification
1056
+ node_ids = set(workflow.graph.nodes()) if workflow else set()
1057
+
1058
+ for key, value in parameters.items():
1059
+ # Node-specific parameter: key is a node ID and value is a dict
1060
+ if key in node_ids and isinstance(value, dict):
1061
+ node_specific_params[key] = value
1062
+ # Workflow-level parameter: key is not a node ID or value is not a dict
1063
+ else:
1064
+ workflow_level_params[key] = value
1065
+
1066
+ if self.debug:
1067
+ self.logger.debug(
1068
+ f"Separated parameters: "
1069
+ f"node_specific={list(node_specific_params.keys())}, "
1070
+ f"workflow_level={list(workflow_level_params.keys())}"
1071
+ )
1072
+
1073
+ return node_specific_params, workflow_level_params
1074
+
1075
+ def _is_node_specific_format(
1076
+ self, parameters: dict[str, Any], workflow: Workflow = None
1077
+ ) -> bool:
1078
+ """Detect if parameters are in node-specific format.
1079
+
1080
+ Node-specific format has structure: {node_id: {param: value}}
1081
+ Workflow-level format has structure: {param: value}
1082
+
1083
+ Args:
1084
+ parameters: Parameters to check
1085
+ workflow: Optional workflow for node ID validation
1086
+
1087
+ Returns:
1088
+ True if node-specific format, False if workflow-level
1089
+ """
1090
+ if not parameters:
1091
+ return True
1092
+
1093
+ # Get node IDs if workflow provided
1094
+ node_ids = set(workflow.graph.nodes()) if workflow else set()
1095
+
1096
+ # If any key is a node ID and its value is a dict, it's node-specific
1097
+ for key, value in parameters.items():
1098
+ if key in node_ids and isinstance(value, dict):
1099
+ return True
1100
+
1101
+ # Additional heuristic: if all values are dicts and keys look like IDs
1102
+ all_dict_values = all(isinstance(v, dict) for v in parameters.values())
1103
+ keys_look_like_ids = any(
1104
+ "_" in k or k.startswith("node") or k in node_ids for k in parameters.keys()
1105
+ )
1106
+
1107
+ if all_dict_values and keys_look_like_ids:
1108
+ return True
1109
+
1110
+ # Default to workflow-level format
1111
+ return False
@@ -0,0 +1,425 @@
1
+ """Parameter injection framework for enterprise nodes.
2
+
3
+ This module provides a framework for handling runtime parameter injection
4
+ in enterprise nodes that traditionally require initialization-time configuration.
5
+ It bridges the gap between static configuration and dynamic workflow parameters.
6
+ """
7
+
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any, Dict, Optional, Union
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ParameterInjectionMixin:
16
+ """Mixin to add parameter injection capabilities to nodes.
17
+
18
+ This mixin allows nodes to defer configuration until runtime,
19
+ enabling workflow-level parameter injection for connection parameters.
20
+ """
21
+
22
+ def __init__(self, *args, **kwargs):
23
+ """Initialize with deferred configuration support."""
24
+ # Store original kwargs for deferred initialization
25
+ self._deferred_config = kwargs.copy()
26
+ self._is_initialized = False
27
+ self._runtime_config = {}
28
+ super().__init__(*args, **kwargs)
29
+
30
+ def set_runtime_parameters(self, **runtime_params: Any) -> None:
31
+ """Set runtime parameters for deferred initialization.
32
+
33
+ Args:
34
+ **runtime_params: Runtime parameters to use for configuration
35
+ """
36
+ self._runtime_config.update(runtime_params)
37
+ logger.debug(
38
+ f"Set runtime parameters for {self.__class__.__name__}: {list(runtime_params.keys())}"
39
+ )
40
+
41
+ def get_effective_config(self) -> Dict[str, Any]:
42
+ """Get effective configuration combining init-time and runtime parameters.
43
+
44
+ Returns:
45
+ Combined configuration with runtime parameters taking precedence
46
+ """
47
+ effective_config = self._deferred_config.copy()
48
+ effective_config.update(self._runtime_config)
49
+ return effective_config
50
+
51
+ def initialize_with_runtime_config(self) -> None:
52
+ """Initialize the node with runtime configuration.
53
+
54
+ Should be called by subclasses when they need to set up connections
55
+ or other resources that depend on runtime parameters.
56
+ """
57
+ if not self._is_initialized:
58
+ self._perform_initialization(self.get_effective_config())
59
+ self._is_initialized = True
60
+
61
+ @abstractmethod
62
+ def _perform_initialization(self, config: Dict[str, Any]) -> None:
63
+ """Perform actual initialization with effective configuration.
64
+
65
+ Args:
66
+ config: Combined configuration to use for initialization
67
+ """
68
+ pass
69
+
70
+ def validate_inputs(self, **kwargs) -> Dict[str, Any]:
71
+ """Override to inject runtime parameters before validation."""
72
+ # Extract connection parameters from runtime inputs
73
+ connection_params = self._extract_connection_params(kwargs)
74
+ if connection_params:
75
+ self.set_runtime_parameters(**connection_params)
76
+
77
+ # Initialize if we have new runtime parameters
78
+ if connection_params and not self._is_initialized:
79
+ self.initialize_with_runtime_config()
80
+
81
+ return super().validate_inputs(**kwargs)
82
+
83
+ def _extract_connection_params(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
84
+ """Extract connection-related parameters from runtime inputs.
85
+
86
+ Args:
87
+ inputs: Runtime input parameters
88
+
89
+ Returns:
90
+ Dictionary of connection parameters
91
+ """
92
+ # Define connection parameter patterns
93
+ connection_param_patterns = [
94
+ "host",
95
+ "port",
96
+ "database",
97
+ "user",
98
+ "password",
99
+ "connection_string",
100
+ "token_url",
101
+ "client_id",
102
+ "client_secret",
103
+ "auth_url",
104
+ "api_key",
105
+ "database_type",
106
+ "grant_type",
107
+ "scope",
108
+ "username",
109
+ ]
110
+
111
+ connection_params = {}
112
+ for key, value in inputs.items():
113
+ if key in connection_param_patterns:
114
+ connection_params[key] = value
115
+
116
+ return connection_params
117
+
118
+
119
+ class ConfigurableOAuth2Node:
120
+ """OAuth2Node with runtime parameter injection support.
121
+
122
+ This class extends OAuth2Node to support runtime configuration of
123
+ connection parameters through workflow parameter injection.
124
+ """
125
+
126
+ def __init__(self, **kwargs):
127
+ """Initialize with deferred OAuth configuration."""
128
+ from kailash.nodes.base import Node, NodeMetadata
129
+
130
+ # Store configuration
131
+ self._deferred_config = kwargs.copy()
132
+ self._runtime_config = {}
133
+ self._is_initialized = False
134
+ self._oauth_node = None
135
+
136
+ # Initialize metadata
137
+ if "metadata" not in kwargs:
138
+ kwargs["metadata"] = NodeMetadata(
139
+ id=kwargs.get("name", "configurable_oauth2").replace(" ", "_").lower(),
140
+ name=kwargs.get("name", "ConfigurableOAuth2Node"),
141
+ description="OAuth2 node with runtime parameter injection",
142
+ tags={"auth", "oauth2", "configurable"},
143
+ version="1.0.0",
144
+ )
145
+
146
+ # Store metadata for later use
147
+ self.metadata = kwargs["metadata"]
148
+
149
+ def _perform_initialization(self, config: Dict[str, Any]) -> None:
150
+ """Initialize OAuth2Node with runtime configuration."""
151
+ from kailash.nodes.api.auth import OAuth2Node
152
+
153
+ # Filter out non-OAuth parameters
154
+ oauth_config = {}
155
+ oauth_params = [
156
+ "token_url",
157
+ "client_id",
158
+ "client_secret",
159
+ "grant_type",
160
+ "scope",
161
+ "username",
162
+ "password",
163
+ "refresh_token",
164
+ ]
165
+
166
+ for param in oauth_params:
167
+ if param in config:
168
+ oauth_config[param] = config[param]
169
+
170
+ # Create the actual OAuth2Node
171
+ self._oauth_node = OAuth2Node(**oauth_config)
172
+ logger.info(
173
+ f"Initialized OAuth2Node with runtime config: {list(oauth_config.keys())}"
174
+ )
175
+
176
+ def get_parameters(self):
177
+ """Get parameters from the underlying OAuth2Node or default set."""
178
+ if self._oauth_node:
179
+ return self._oauth_node.get_parameters()
180
+ else:
181
+ # Return default OAuth2 parameters for workflow building
182
+ from kailash.nodes.base import NodeParameter
183
+
184
+ return {
185
+ "token_url": NodeParameter(
186
+ name="token_url",
187
+ type=str,
188
+ required=True,
189
+ description="OAuth token endpoint URL",
190
+ ),
191
+ "client_id": NodeParameter(
192
+ name="client_id",
193
+ type=str,
194
+ required=True,
195
+ description="OAuth client ID",
196
+ ),
197
+ "client_secret": NodeParameter(
198
+ name="client_secret",
199
+ type=str,
200
+ required=False,
201
+ description="OAuth client secret",
202
+ ),
203
+ "grant_type": NodeParameter(
204
+ name="grant_type",
205
+ type=str,
206
+ required=False,
207
+ default="client_credentials",
208
+ description="OAuth grant type",
209
+ ),
210
+ }
211
+
212
+ def run(self, **kwargs):
213
+ """Execute OAuth2 authentication with runtime configuration."""
214
+ # Ensure initialization with current parameters
215
+ if not self._is_initialized:
216
+ self.set_runtime_parameters(**kwargs)
217
+ self.initialize_with_runtime_config()
218
+
219
+ if not self._oauth_node:
220
+ raise RuntimeError(
221
+ "OAuth2Node not initialized - missing connection parameters"
222
+ )
223
+
224
+ # Delegate to the initialized OAuth2Node
225
+ return self._oauth_node.execute(**kwargs)
226
+
227
+
228
+ class ConfigurableAsyncSQLNode(ParameterInjectionMixin):
229
+ """AsyncSQLDatabaseNode with runtime parameter injection support.
230
+
231
+ This class extends AsyncSQLDatabaseNode to support runtime configuration
232
+ of database connection parameters through workflow parameter injection.
233
+ """
234
+
235
+ def __init__(self, **kwargs):
236
+ """Initialize with deferred database configuration."""
237
+ from kailash.nodes.data.async_sql import AsyncSQLDatabaseNode
238
+
239
+ # Don't initialize AsyncSQLDatabaseNode yet - defer until runtime
240
+ self._sql_node = None
241
+ super().__init__(**kwargs)
242
+
243
+ def _perform_initialization(self, config: Dict[str, Any]) -> None:
244
+ """Initialize AsyncSQLDatabaseNode with runtime configuration."""
245
+ from kailash.nodes.data.async_sql import AsyncSQLDatabaseNode
246
+
247
+ # Filter out non-SQL parameters
248
+ sql_config = {}
249
+ sql_params = [
250
+ "database_type",
251
+ "connection_string",
252
+ "host",
253
+ "port",
254
+ "database",
255
+ "user",
256
+ "password",
257
+ "pool_size",
258
+ "max_pool_size",
259
+ "timeout",
260
+ ]
261
+
262
+ for param in sql_params:
263
+ if param in config:
264
+ sql_config[param] = config[param]
265
+
266
+ # Add any query parameters
267
+ if "query" in config:
268
+ sql_config["query"] = config["query"]
269
+ if "params" in config:
270
+ sql_config["params"] = config["params"]
271
+ if "fetch_mode" in config:
272
+ sql_config["fetch_mode"] = config["fetch_mode"]
273
+
274
+ # Create the actual AsyncSQLDatabaseNode
275
+ self._sql_node = AsyncSQLDatabaseNode(**sql_config)
276
+ logger.info(
277
+ f"Initialized AsyncSQLDatabaseNode with runtime config: {list(sql_config.keys())}"
278
+ )
279
+
280
+ def get_parameters(self):
281
+ """Get parameters from the underlying AsyncSQLDatabaseNode or default set."""
282
+ if self._sql_node:
283
+ return self._sql_node.get_parameters()
284
+ else:
285
+ # Return default SQL parameters for workflow building
286
+ from kailash.nodes.base import NodeParameter
287
+
288
+ return {
289
+ "database_type": NodeParameter(
290
+ name="database_type",
291
+ type=str,
292
+ required=True,
293
+ default="postgresql",
294
+ description="Type of database",
295
+ ),
296
+ "host": NodeParameter(
297
+ name="host", type=str, required=False, description="Database host"
298
+ ),
299
+ "database": NodeParameter(
300
+ name="database",
301
+ type=str,
302
+ required=False,
303
+ description="Database name",
304
+ ),
305
+ "user": NodeParameter(
306
+ name="user", type=str, required=False, description="Database user"
307
+ ),
308
+ "password": NodeParameter(
309
+ name="password",
310
+ type=str,
311
+ required=False,
312
+ description="Database password",
313
+ ),
314
+ "query": NodeParameter(
315
+ name="query",
316
+ type=str,
317
+ required=True,
318
+ description="SQL query to execute",
319
+ ),
320
+ }
321
+
322
+ async def async_run(self, **kwargs):
323
+ """Execute SQL query with runtime configuration."""
324
+ # Ensure initialization with current parameters
325
+ if not self._is_initialized:
326
+ self.set_runtime_parameters(**kwargs)
327
+ self.initialize_with_runtime_config()
328
+
329
+ if not self._sql_node:
330
+ raise RuntimeError(
331
+ "AsyncSQLDatabaseNode not initialized - missing connection parameters"
332
+ )
333
+
334
+ # Delegate to the initialized AsyncSQLDatabaseNode
335
+ return await self._sql_node.async_run(**kwargs)
336
+
337
+ def run(self, **kwargs):
338
+ """Synchronous wrapper for async_run."""
339
+ import asyncio
340
+
341
+ return asyncio.run(self.async_run(**kwargs))
342
+
343
+
344
+ class EnterpriseNodeFactory:
345
+ """Factory for creating enterprise nodes with parameter injection support."""
346
+
347
+ @staticmethod
348
+ def create_oauth2_node(**kwargs) -> ConfigurableOAuth2Node:
349
+ """Create an OAuth2Node with runtime parameter injection support.
350
+
351
+ Args:
352
+ **kwargs: Initial configuration parameters
353
+
354
+ Returns:
355
+ ConfigurableOAuth2Node instance
356
+ """
357
+ return ConfigurableOAuth2Node(**kwargs)
358
+
359
+ @staticmethod
360
+ def create_async_sql_node(**kwargs) -> ConfigurableAsyncSQLNode:
361
+ """Create an AsyncSQLDatabaseNode with runtime parameter injection support.
362
+
363
+ Args:
364
+ **kwargs: Initial configuration parameters
365
+
366
+ Returns:
367
+ ConfigurableAsyncSQLNode instance
368
+ """
369
+ return ConfigurableAsyncSQLNode(**kwargs)
370
+
371
+ @staticmethod
372
+ def wrap_enterprise_node(node_class, **kwargs):
373
+ """Wrap any enterprise node class with parameter injection support.
374
+
375
+ Args:
376
+ node_class: The enterprise node class to wrap
377
+ **kwargs: Initial configuration parameters
378
+
379
+ Returns:
380
+ Wrapped node instance with parameter injection support
381
+ """
382
+
383
+ # Create a dynamic wrapper class
384
+ class WrappedEnterpriseNode(ParameterInjectionMixin):
385
+ def __init__(self, **init_kwargs):
386
+ self._wrapped_node = None
387
+ self._node_class = node_class
388
+ super().__init__(**init_kwargs)
389
+
390
+ def _perform_initialization(self, config):
391
+ self._wrapped_node = self._node_class(**config)
392
+
393
+ def get_parameters(self):
394
+ if self._wrapped_node:
395
+ return self._wrapped_node.get_parameters()
396
+ else:
397
+ # Try to get parameters from class if possible
398
+ try:
399
+ temp_node = self._node_class()
400
+ return temp_node.get_parameters()
401
+ except:
402
+ return {}
403
+
404
+ def run(self, **run_kwargs):
405
+ if not self._is_initialized:
406
+ self.set_runtime_parameters(**run_kwargs)
407
+ self.initialize_with_runtime_config()
408
+
409
+ if not self._wrapped_node:
410
+ raise RuntimeError(f"{self._node_class.__name__} not initialized")
411
+
412
+ return self._wrapped_node.execute(**run_kwargs)
413
+
414
+ return WrappedEnterpriseNode(**kwargs)
415
+
416
+
417
+ # Convenience functions for workflow builders
418
+ def create_configurable_oauth2(**kwargs) -> ConfigurableOAuth2Node:
419
+ """Convenience function to create a configurable OAuth2 node."""
420
+ return EnterpriseNodeFactory.create_oauth2_node(**kwargs)
421
+
422
+
423
+ def create_configurable_sql(**kwargs) -> ConfigurableAsyncSQLNode:
424
+ """Convenience function to create a configurable SQL node."""
425
+ return EnterpriseNodeFactory.create_async_sql_node(**kwargs)