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.
- kailash/__init__.py +3 -3
- kailash/api/custom_nodes_secure.py +3 -3
- kailash/api/gateway.py +1 -1
- kailash/api/studio.py +2 -3
- kailash/api/workflow_api.py +3 -4
- kailash/core/resilience/bulkhead.py +460 -0
- kailash/core/resilience/circuit_breaker.py +92 -10
- kailash/edge/discovery.py +86 -0
- kailash/mcp_server/__init__.py +309 -33
- kailash/mcp_server/advanced_features.py +1022 -0
- kailash/mcp_server/ai_registry_server.py +27 -2
- kailash/mcp_server/auth.py +789 -0
- kailash/mcp_server/client.py +645 -378
- kailash/mcp_server/discovery.py +1593 -0
- kailash/mcp_server/errors.py +673 -0
- kailash/mcp_server/oauth.py +1727 -0
- kailash/mcp_server/protocol.py +1126 -0
- kailash/mcp_server/registry_integration.py +587 -0
- kailash/mcp_server/server.py +1213 -98
- kailash/mcp_server/transports.py +1169 -0
- kailash/mcp_server/utils/__init__.py +6 -1
- kailash/mcp_server/utils/cache.py +250 -7
- kailash/middleware/auth/auth_manager.py +3 -3
- kailash/middleware/communication/api_gateway.py +2 -9
- kailash/middleware/communication/realtime.py +1 -1
- kailash/middleware/mcp/enhanced_server.py +1 -1
- kailash/nodes/__init__.py +2 -0
- kailash/nodes/admin/audit_log.py +6 -6
- kailash/nodes/admin/permission_check.py +8 -8
- kailash/nodes/admin/role_management.py +32 -28
- kailash/nodes/admin/schema.sql +6 -1
- kailash/nodes/admin/schema_manager.py +13 -13
- kailash/nodes/admin/security_event.py +16 -20
- kailash/nodes/admin/tenant_isolation.py +3 -3
- kailash/nodes/admin/transaction_utils.py +3 -3
- kailash/nodes/admin/user_management.py +21 -22
- kailash/nodes/ai/a2a.py +11 -11
- kailash/nodes/ai/ai_providers.py +9 -12
- kailash/nodes/ai/embedding_generator.py +13 -14
- kailash/nodes/ai/intelligent_agent_orchestrator.py +19 -19
- kailash/nodes/ai/iterative_llm_agent.py +2 -2
- kailash/nodes/ai/llm_agent.py +210 -33
- kailash/nodes/ai/self_organizing.py +2 -2
- kailash/nodes/alerts/discord.py +4 -4
- kailash/nodes/api/graphql.py +6 -6
- kailash/nodes/api/http.py +12 -17
- kailash/nodes/api/rate_limiting.py +4 -4
- kailash/nodes/api/rest.py +15 -15
- kailash/nodes/auth/mfa.py +3 -4
- kailash/nodes/auth/risk_assessment.py +2 -2
- kailash/nodes/auth/session_management.py +5 -5
- kailash/nodes/auth/sso.py +143 -0
- kailash/nodes/base.py +6 -2
- kailash/nodes/base_async.py +16 -2
- kailash/nodes/base_with_acl.py +2 -2
- kailash/nodes/cache/__init__.py +9 -0
- kailash/nodes/cache/cache.py +1172 -0
- kailash/nodes/cache/cache_invalidation.py +870 -0
- kailash/nodes/cache/redis_pool_manager.py +595 -0
- kailash/nodes/code/async_python.py +2 -1
- kailash/nodes/code/python.py +196 -35
- kailash/nodes/compliance/data_retention.py +6 -6
- kailash/nodes/compliance/gdpr.py +5 -5
- kailash/nodes/data/__init__.py +10 -0
- kailash/nodes/data/optimistic_locking.py +906 -0
- kailash/nodes/data/readers.py +8 -8
- kailash/nodes/data/redis.py +349 -0
- kailash/nodes/data/sql.py +314 -3
- kailash/nodes/data/streaming.py +21 -0
- kailash/nodes/enterprise/__init__.py +8 -0
- kailash/nodes/enterprise/audit_logger.py +285 -0
- kailash/nodes/enterprise/batch_processor.py +22 -3
- kailash/nodes/enterprise/data_lineage.py +1 -1
- kailash/nodes/enterprise/mcp_executor.py +205 -0
- kailash/nodes/enterprise/service_discovery.py +150 -0
- kailash/nodes/enterprise/tenant_assignment.py +108 -0
- kailash/nodes/logic/async_operations.py +2 -2
- kailash/nodes/logic/convergence.py +1 -1
- kailash/nodes/logic/operations.py +1 -1
- kailash/nodes/monitoring/__init__.py +11 -1
- kailash/nodes/monitoring/health_check.py +456 -0
- kailash/nodes/monitoring/log_processor.py +817 -0
- kailash/nodes/monitoring/metrics_collector.py +627 -0
- kailash/nodes/monitoring/performance_benchmark.py +137 -11
- kailash/nodes/rag/advanced.py +7 -7
- kailash/nodes/rag/agentic.py +49 -2
- kailash/nodes/rag/conversational.py +3 -3
- kailash/nodes/rag/evaluation.py +3 -3
- kailash/nodes/rag/federated.py +3 -3
- kailash/nodes/rag/graph.py +3 -3
- kailash/nodes/rag/multimodal.py +3 -3
- kailash/nodes/rag/optimized.py +5 -5
- kailash/nodes/rag/privacy.py +3 -3
- kailash/nodes/rag/query_processing.py +6 -6
- kailash/nodes/rag/realtime.py +1 -1
- kailash/nodes/rag/registry.py +2 -6
- kailash/nodes/rag/router.py +1 -1
- kailash/nodes/rag/similarity.py +7 -7
- kailash/nodes/rag/strategies.py +4 -4
- kailash/nodes/security/abac_evaluator.py +6 -6
- kailash/nodes/security/behavior_analysis.py +5 -6
- kailash/nodes/security/credential_manager.py +1 -1
- kailash/nodes/security/rotating_credentials.py +11 -11
- kailash/nodes/security/threat_detection.py +8 -8
- kailash/nodes/testing/credential_testing.py +2 -2
- kailash/nodes/transform/processors.py +5 -5
- kailash/runtime/local.py +162 -14
- kailash/runtime/parameter_injection.py +425 -0
- kailash/runtime/parameter_injector.py +657 -0
- kailash/runtime/testing.py +2 -2
- kailash/testing/fixtures.py +2 -2
- kailash/workflow/builder.py +99 -18
- kailash/workflow/builder_improvements.py +207 -0
- kailash/workflow/input_handling.py +170 -0
- {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/METADATA +22 -9
- {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/RECORD +120 -94
- {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/WHEEL +0 -0
- {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.3.dist-info → kailash-0.6.4.dist-info}/licenses/LICENSE +0 -0
- {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=
|
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":
|
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":
|
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,
|
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=
|
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)
|