kailash 0.3.0__py3-none-any.whl → 0.3.2__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 (113) hide show
  1. kailash/access_control.py +40 -39
  2. kailash/api/auth.py +26 -32
  3. kailash/api/custom_nodes.py +29 -29
  4. kailash/api/custom_nodes_secure.py +35 -35
  5. kailash/api/database.py +17 -17
  6. kailash/api/gateway.py +19 -19
  7. kailash/api/mcp_integration.py +24 -23
  8. kailash/api/studio.py +45 -45
  9. kailash/api/workflow_api.py +8 -8
  10. kailash/cli/commands.py +5 -8
  11. kailash/manifest.py +42 -42
  12. kailash/mcp/__init__.py +1 -1
  13. kailash/mcp/ai_registry_server.py +20 -20
  14. kailash/mcp/client.py +9 -11
  15. kailash/mcp/client_new.py +10 -10
  16. kailash/mcp/server.py +1 -2
  17. kailash/mcp/server_enhanced.py +449 -0
  18. kailash/mcp/servers/ai_registry.py +6 -6
  19. kailash/mcp/utils/__init__.py +31 -0
  20. kailash/mcp/utils/cache.py +267 -0
  21. kailash/mcp/utils/config.py +263 -0
  22. kailash/mcp/utils/formatters.py +293 -0
  23. kailash/mcp/utils/metrics.py +418 -0
  24. kailash/nodes/ai/agents.py +9 -9
  25. kailash/nodes/ai/ai_providers.py +33 -34
  26. kailash/nodes/ai/embedding_generator.py +31 -32
  27. kailash/nodes/ai/intelligent_agent_orchestrator.py +62 -66
  28. kailash/nodes/ai/iterative_llm_agent.py +48 -48
  29. kailash/nodes/ai/llm_agent.py +32 -33
  30. kailash/nodes/ai/models.py +13 -13
  31. kailash/nodes/ai/self_organizing.py +44 -44
  32. kailash/nodes/api/auth.py +11 -11
  33. kailash/nodes/api/graphql.py +13 -13
  34. kailash/nodes/api/http.py +19 -19
  35. kailash/nodes/api/monitoring.py +20 -20
  36. kailash/nodes/api/rate_limiting.py +9 -13
  37. kailash/nodes/api/rest.py +29 -29
  38. kailash/nodes/api/security.py +44 -47
  39. kailash/nodes/base.py +21 -23
  40. kailash/nodes/base_async.py +7 -7
  41. kailash/nodes/base_cycle_aware.py +12 -12
  42. kailash/nodes/base_with_acl.py +5 -5
  43. kailash/nodes/code/python.py +66 -57
  44. kailash/nodes/data/directory.py +6 -6
  45. kailash/nodes/data/event_generation.py +10 -10
  46. kailash/nodes/data/file_discovery.py +28 -31
  47. kailash/nodes/data/readers.py +8 -8
  48. kailash/nodes/data/retrieval.py +10 -10
  49. kailash/nodes/data/sharepoint_graph.py +17 -17
  50. kailash/nodes/data/sources.py +5 -5
  51. kailash/nodes/data/sql.py +13 -13
  52. kailash/nodes/data/streaming.py +25 -25
  53. kailash/nodes/data/vector_db.py +22 -22
  54. kailash/nodes/data/writers.py +7 -7
  55. kailash/nodes/logic/async_operations.py +17 -17
  56. kailash/nodes/logic/convergence.py +11 -11
  57. kailash/nodes/logic/loop.py +4 -4
  58. kailash/nodes/logic/operations.py +11 -11
  59. kailash/nodes/logic/workflow.py +8 -9
  60. kailash/nodes/mixins/mcp.py +17 -17
  61. kailash/nodes/mixins.py +8 -10
  62. kailash/nodes/transform/chunkers.py +3 -3
  63. kailash/nodes/transform/formatters.py +7 -7
  64. kailash/nodes/transform/processors.py +10 -10
  65. kailash/runtime/access_controlled.py +18 -18
  66. kailash/runtime/async_local.py +17 -19
  67. kailash/runtime/docker.py +20 -22
  68. kailash/runtime/local.py +16 -16
  69. kailash/runtime/parallel.py +23 -23
  70. kailash/runtime/parallel_cyclic.py +27 -27
  71. kailash/runtime/runner.py +6 -6
  72. kailash/runtime/testing.py +20 -20
  73. kailash/sdk_exceptions.py +0 -58
  74. kailash/security.py +14 -26
  75. kailash/tracking/manager.py +38 -38
  76. kailash/tracking/metrics_collector.py +15 -14
  77. kailash/tracking/models.py +53 -53
  78. kailash/tracking/storage/base.py +7 -17
  79. kailash/tracking/storage/database.py +22 -23
  80. kailash/tracking/storage/filesystem.py +38 -40
  81. kailash/utils/export.py +21 -21
  82. kailash/utils/templates.py +2 -3
  83. kailash/visualization/api.py +30 -34
  84. kailash/visualization/dashboard.py +17 -17
  85. kailash/visualization/performance.py +16 -16
  86. kailash/visualization/reports.py +25 -27
  87. kailash/workflow/builder.py +8 -8
  88. kailash/workflow/convergence.py +13 -12
  89. kailash/workflow/cycle_analyzer.py +30 -32
  90. kailash/workflow/cycle_builder.py +12 -12
  91. kailash/workflow/cycle_config.py +16 -15
  92. kailash/workflow/cycle_debugger.py +40 -40
  93. kailash/workflow/cycle_exceptions.py +29 -29
  94. kailash/workflow/cycle_profiler.py +21 -21
  95. kailash/workflow/cycle_state.py +20 -22
  96. kailash/workflow/cyclic_runner.py +44 -44
  97. kailash/workflow/graph.py +40 -40
  98. kailash/workflow/mermaid_visualizer.py +9 -11
  99. kailash/workflow/migration.py +22 -22
  100. kailash/workflow/mock_registry.py +6 -6
  101. kailash/workflow/runner.py +9 -9
  102. kailash/workflow/safety.py +12 -13
  103. kailash/workflow/state.py +8 -11
  104. kailash/workflow/templates.py +19 -19
  105. kailash/workflow/validation.py +14 -14
  106. kailash/workflow/visualization.py +22 -22
  107. {kailash-0.3.0.dist-info → kailash-0.3.2.dist-info}/METADATA +53 -5
  108. kailash-0.3.2.dist-info/RECORD +136 -0
  109. kailash-0.3.0.dist-info/RECORD +0 -130
  110. {kailash-0.3.0.dist-info → kailash-0.3.2.dist-info}/WHEEL +0 -0
  111. {kailash-0.3.0.dist-info → kailash-0.3.2.dist-info}/entry_points.txt +0 -0
  112. {kailash-0.3.0.dist-info → kailash-0.3.2.dist-info}/licenses/LICENSE +0 -0
  113. {kailash-0.3.0.dist-info → kailash-0.3.2.dist-info}/top_level.txt +0 -0
kailash/nodes/base.py CHANGED
@@ -21,8 +21,8 @@ Key Components:
21
21
  import json
22
22
  import logging
23
23
  from abc import ABC, abstractmethod
24
- from datetime import datetime, timezone
25
- from typing import Any, Dict, Optional, Set, Type
24
+ from datetime import UTC, datetime
25
+ from typing import Any
26
26
 
27
27
  from pydantic import BaseModel, Field, ValidationError
28
28
 
@@ -60,10 +60,10 @@ class NodeMetadata(BaseModel):
60
60
  version: str = Field("1.0.0", description="Node version")
61
61
  author: str = Field("", description="Node author")
62
62
  created_at: datetime = Field(
63
- default_factory=lambda: datetime.now(timezone.utc),
63
+ default_factory=lambda: datetime.now(UTC),
64
64
  description="Node creation date",
65
65
  )
66
- tags: Set[str] = Field(default_factory=set, description="Node tags")
66
+ tags: set[str] = Field(default_factory=set, description="Node tags")
67
67
 
68
68
 
69
69
  class NodeParameter(BaseModel):
@@ -94,7 +94,7 @@ class NodeParameter(BaseModel):
94
94
  """
95
95
 
96
96
  name: str
97
- type: Type
97
+ type: type
98
98
  required: bool = True
99
99
  default: Any = None
100
100
  description: str = ""
@@ -208,7 +208,7 @@ class Node(ABC):
208
208
  ) from e
209
209
 
210
210
  @abstractmethod
211
- def get_parameters(self) -> Dict[str, NodeParameter]:
211
+ def get_parameters(self) -> dict[str, NodeParameter]:
212
212
  """Define the parameters this node accepts.
213
213
 
214
214
  This abstract method must be implemented by all concrete nodes to
@@ -254,9 +254,8 @@ class Node(ABC):
254
254
  - to_dict(): Includes parameters in serialization
255
255
  - Workflow.connect(): Validates compatible connections
256
256
  """
257
- pass
258
257
 
259
- def get_output_schema(self) -> Dict[str, NodeParameter]:
258
+ def get_output_schema(self) -> dict[str, NodeParameter]:
260
259
  """Define output parameters for this node.
261
260
 
262
261
  This optional method allows nodes to specify their output schema for
@@ -313,7 +312,7 @@ class Node(ABC):
313
312
  return {}
314
313
 
315
314
  @abstractmethod
316
- def run(self, **kwargs) -> Dict[str, Any]:
315
+ def run(self, **kwargs) -> dict[str, Any]:
317
316
  """Execute the node's logic.
318
317
 
319
318
  This is the core method that implements the node's data processing
@@ -361,7 +360,6 @@ class Node(ABC):
361
360
  - LocalRuntime: During workflow execution
362
361
  - TestRunner: During unit testing
363
362
  """
364
- pass
365
363
 
366
364
  def _validate_config(self):
367
365
  """Validate node configuration against defined parameters.
@@ -428,8 +426,8 @@ class Node(ABC):
428
426
  f"Conversion failed: {e}"
429
427
  ) from e
430
428
 
431
- def validate_inputs(self, **kwargs) -> Dict[str, Any]:
432
- """Validate runtime inputs against node requirements.
429
+ def validate_inputs(self, **kwargs) -> dict[str, Any]:
430
+ r"""Validate runtime inputs against node requirements.
433
431
 
434
432
  This method validates inputs provided at execution time against the
435
433
  node's parameter schema. It ensures type safety and provides helpful
@@ -521,7 +519,7 @@ class Node(ABC):
521
519
 
522
520
  return validated
523
521
 
524
- def validate_outputs(self, outputs: Dict[str, Any]) -> Dict[str, Any]:
522
+ def validate_outputs(self, outputs: dict[str, Any]) -> dict[str, Any]:
525
523
  """Validate outputs against schema and JSON-serializability.
526
524
 
527
525
  This enhanced method validates outputs in two ways:
@@ -661,7 +659,7 @@ class Node(ABC):
661
659
  except (TypeError, ValueError):
662
660
  return False
663
661
 
664
- def execute(self, **runtime_inputs) -> Dict[str, Any]:
662
+ def execute(self, **runtime_inputs) -> dict[str, Any]:
665
663
  """Execute the node with validation and error handling.
666
664
 
667
665
  This is the main entry point for node execution that orchestrates
@@ -711,7 +709,7 @@ class Node(ABC):
711
709
  - Metrics enable performance monitoring
712
710
  - Validation ensures data integrity
713
711
  """
714
- start_time = datetime.now(timezone.utc)
712
+ start_time = datetime.now(UTC)
715
713
  try:
716
714
  self.logger.info(f"Executing node {self.id}")
717
715
 
@@ -735,7 +733,7 @@ class Node(ABC):
735
733
  # Validate outputs
736
734
  validated_outputs = self.validate_outputs(outputs)
737
735
 
738
- execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
736
+ execution_time = (datetime.now(UTC) - start_time).total_seconds()
739
737
  self.logger.info(
740
738
  f"Node {self.id} executed successfully in {execution_time:.3f}s"
741
739
  )
@@ -754,7 +752,7 @@ class Node(ABC):
754
752
  f"Node '{self.id}' execution failed: {type(e).__name__}: {e}"
755
753
  ) from e
756
754
 
757
- def to_dict(self) -> Dict[str, Any]:
755
+ def to_dict(self) -> dict[str, Any]:
758
756
  """Convert node to dictionary representation.
759
757
 
760
758
  Serializes the node instance to a dictionary format suitable for:
@@ -866,7 +864,7 @@ class NodeRegistry:
866
864
  """
867
865
 
868
866
  _instance = None
869
- _nodes: Dict[str, Type[Node]] = {}
867
+ _nodes: dict[str, type[Node]] = {}
870
868
 
871
869
  def __new__(cls):
872
870
  """Ensure singleton instance.
@@ -882,7 +880,7 @@ class NodeRegistry:
882
880
  return cls._instance
883
881
 
884
882
  @classmethod
885
- def register(cls, node_class: Type[Node], alias: Optional[str] = None):
883
+ def register(cls, node_class: type[Node], alias: str | None = None):
886
884
  """Register a node class.
887
885
 
888
886
  Adds a node class to the global registry, making it available
@@ -940,7 +938,7 @@ class NodeRegistry:
940
938
  logging.info(f"Registered node '{node_name}'")
941
939
 
942
940
  @classmethod
943
- def get(cls, node_name: str) -> Type[Node]:
941
+ def get(cls, node_name: str) -> type[Node]:
944
942
  """Get a registered node class by name.
945
943
 
946
944
  Retrieves a node class from the registry for instantiation.
@@ -983,7 +981,7 @@ class NodeRegistry:
983
981
  return cls._nodes[node_name]
984
982
 
985
983
  @classmethod
986
- def list_nodes(cls) -> Dict[str, Type[Node]]:
984
+ def list_nodes(cls) -> dict[str, type[Node]]:
987
985
  """List all registered nodes.
988
986
 
989
987
  Returns a copy of the registry for discovery purposes.
@@ -1030,7 +1028,7 @@ class NodeRegistry:
1030
1028
  logging.info("Cleared all registered nodes")
1031
1029
 
1032
1030
 
1033
- def register_node(alias: Optional[str] = None):
1031
+ def register_node(alias: str | None = None):
1034
1032
  """Decorator to register a node class.
1035
1033
 
1036
1034
  Provides a convenient decorator pattern for automatic node
@@ -1077,7 +1075,7 @@ def register_node(alias: Optional[str] = None):
1077
1075
  ... return pd.read_csv(file)
1078
1076
  """
1079
1077
 
1080
- def decorator(node_class: Type[Node]):
1078
+ def decorator(node_class: type[Node]):
1081
1079
  """Inner decorator that performs registration.
1082
1080
 
1083
1081
  Args:
@@ -4,8 +4,8 @@ This module extends the base Node class with asynchronous execution capabilities
4
4
  allowing for more efficient handling of I/O-bound operations in workflows.
5
5
  """
6
6
 
7
- from datetime import datetime, timezone
8
- from typing import Any, Dict
7
+ from datetime import UTC, datetime
8
+ from typing import Any
9
9
 
10
10
  from kailash.nodes.base import Node
11
11
  from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
@@ -45,7 +45,7 @@ class AsyncNode(Node):
45
45
  - TaskManager: Tracks node execution status
46
46
  """
47
47
 
48
- def execute(self, **runtime_inputs) -> Dict[str, Any]:
48
+ def execute(self, **runtime_inputs) -> dict[str, Any]:
49
49
  """Execute the node synchronously by running async code in a new event loop.
50
50
 
51
51
  This override allows AsyncNode to work with synchronous runtimes like LocalRuntime.
@@ -90,7 +90,7 @@ class AsyncNode(Node):
90
90
  asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
91
91
  return asyncio.run(self.execute_async(**runtime_inputs))
92
92
 
93
- async def async_run(self, **kwargs) -> Dict[str, Any]:
93
+ async def async_run(self, **kwargs) -> dict[str, Any]:
94
94
  """Asynchronous execution method for the node.
95
95
 
96
96
  This method should be overridden by subclasses that require asynchronous
@@ -108,7 +108,7 @@ class AsyncNode(Node):
108
108
  # Default implementation calls the synchronous run() method
109
109
  return self.run(**kwargs)
110
110
 
111
- async def execute_async(self, **runtime_inputs) -> Dict[str, Any]:
111
+ async def execute_async(self, **runtime_inputs) -> dict[str, Any]:
112
112
  """Execute the node asynchronously with validation and error handling.
113
113
 
114
114
  This method follows the same pattern as execute() but supports asynchronous
@@ -129,7 +129,7 @@ class AsyncNode(Node):
129
129
  NodeValidationError: If inputs or outputs are invalid
130
130
  NodeExecutionError: If execution fails
131
131
  """
132
- start_time = datetime.now(timezone.utc)
132
+ start_time = datetime.now(UTC)
133
133
  try:
134
134
  self.logger.info(f"Executing node {self.id} asynchronously")
135
135
 
@@ -153,7 +153,7 @@ class AsyncNode(Node):
153
153
  # Validate outputs
154
154
  validated_outputs = self.validate_outputs(outputs)
155
155
 
156
- execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
156
+ execution_time = (datetime.now(UTC) - start_time).total_seconds()
157
157
  self.logger.info(
158
158
  f"Node {self.id} executed successfully in {execution_time:.3f}s"
159
159
  )
@@ -37,7 +37,7 @@ Example usage:
37
37
  """
38
38
 
39
39
  import time
40
- from typing import Any, Dict
40
+ from typing import Any
41
41
 
42
42
  from .base import Node
43
43
 
@@ -103,7 +103,7 @@ class CycleAwareNode(Node):
103
103
  ... }
104
104
  """
105
105
 
106
- def get_cycle_info(self, context: Dict[str, Any]) -> Dict[str, Any]:
106
+ def get_cycle_info(self, context: dict[str, Any]) -> dict[str, Any]:
107
107
  """
108
108
  Get cycle information with sensible defaults.
109
109
 
@@ -138,7 +138,7 @@ class CycleAwareNode(Node):
138
138
  **cycle_info, # Include any additional cycle information
139
139
  }
140
140
 
141
- def get_iteration(self, context: Dict[str, Any]) -> int:
141
+ def get_iteration(self, context: dict[str, Any]) -> int:
142
142
  """
143
143
  Get current iteration number.
144
144
 
@@ -155,7 +155,7 @@ class CycleAwareNode(Node):
155
155
  """
156
156
  return self.get_cycle_info(context)["iteration"]
157
157
 
158
- def is_first_iteration(self, context: Dict[str, Any]) -> bool:
158
+ def is_first_iteration(self, context: dict[str, Any]) -> bool:
159
159
  """
160
160
  Check if this is the first iteration of the cycle.
161
161
 
@@ -172,7 +172,7 @@ class CycleAwareNode(Node):
172
172
  """
173
173
  return self.get_iteration(context) == 0
174
174
 
175
- def is_last_iteration(self, context: Dict[str, Any]) -> bool:
175
+ def is_last_iteration(self, context: dict[str, Any]) -> bool:
176
176
  """
177
177
  Check if this is the last iteration of the cycle.
178
178
 
@@ -189,7 +189,7 @@ class CycleAwareNode(Node):
189
189
  cycle_info = self.get_cycle_info(context)
190
190
  return cycle_info["iteration"] >= cycle_info["max_iterations"] - 1
191
191
 
192
- def get_previous_state(self, context: Dict[str, Any]) -> Dict[str, Any]:
192
+ def get_previous_state(self, context: dict[str, Any]) -> dict[str, Any]:
193
193
  """
194
194
  Get previous iteration state safely.
195
195
 
@@ -209,7 +209,7 @@ class CycleAwareNode(Node):
209
209
  """
210
210
  return self.get_cycle_info(context).get("node_state", {})
211
211
 
212
- def set_cycle_state(self, state: Dict[str, Any]) -> Dict[str, Any]:
212
+ def set_cycle_state(self, state: dict[str, Any]) -> dict[str, Any]:
213
213
  """
214
214
  Set state to persist to next iteration.
215
215
 
@@ -233,7 +233,7 @@ class CycleAwareNode(Node):
233
233
  """
234
234
  return {"_cycle_state": state}
235
235
 
236
- def get_cycle_progress(self, context: Dict[str, Any]) -> float:
236
+ def get_cycle_progress(self, context: dict[str, Any]) -> float:
237
237
  """
238
238
  Get cycle progress as a percentage.
239
239
 
@@ -256,7 +256,7 @@ class CycleAwareNode(Node):
256
256
 
257
257
  return min(iteration / max_iterations, 1.0)
258
258
 
259
- def log_cycle_info(self, context: Dict[str, Any], message: str = "") -> None:
259
+ def log_cycle_info(self, context: dict[str, Any], message: str = "") -> None:
260
260
  """
261
261
  Log cycle information for debugging.
262
262
 
@@ -284,7 +284,7 @@ class CycleAwareNode(Node):
284
284
 
285
285
  print(log_msg)
286
286
 
287
- def should_continue_cycle(self, context: Dict[str, Any], **kwargs) -> bool:
287
+ def should_continue_cycle(self, context: dict[str, Any], **kwargs) -> bool:
288
288
  """
289
289
  Helper method to determine if cycle should continue.
290
290
 
@@ -307,7 +307,7 @@ class CycleAwareNode(Node):
307
307
  return not self.is_last_iteration(context)
308
308
 
309
309
  def accumulate_values(
310
- self, context: Dict[str, Any], key: str, value: Any, max_history: int = 100
310
+ self, context: dict[str, Any], key: str, value: Any, max_history: int = 100
311
311
  ) -> list:
312
312
  """
313
313
  Accumulate values across iterations with automatic history management.
@@ -340,7 +340,7 @@ class CycleAwareNode(Node):
340
340
 
341
341
  def detect_convergence_trend(
342
342
  self,
343
- context: Dict[str, Any],
343
+ context: dict[str, Any],
344
344
  key: str,
345
345
  threshold: float = 0.01,
346
346
  window: int = 3,
@@ -14,7 +14,7 @@ Key Design Principles:
14
14
  """
15
15
 
16
16
  import logging
17
- from typing import Any, Dict, Optional
17
+ from typing import Any
18
18
 
19
19
  from kailash.access_control import (
20
20
  AccessDecision,
@@ -146,7 +146,7 @@ class NodeWithAccessControl(Node):
146
146
  else:
147
147
  raise NotImplementedError("Node must implement _execute() method")
148
148
 
149
- def _should_check_access(self, user_context: Optional[UserContext]) -> bool:
149
+ def _should_check_access(self, user_context: UserContext | None) -> bool:
150
150
  """
151
151
  Determine if access control should be checked.
152
152
 
@@ -179,7 +179,7 @@ class NodeWithAccessControl(Node):
179
179
  # Fall back to class name
180
180
  return self.__class__.__name__
181
181
 
182
- def _mask_fields(self, data: Dict[str, Any], fields: list[str]) -> Dict[str, Any]:
182
+ def _mask_fields(self, data: dict[str, Any], fields: list[str]) -> dict[str, Any]:
183
183
  """Mask specified fields in output data"""
184
184
  masked_data = data.copy()
185
185
  for field in fields:
@@ -188,7 +188,7 @@ class NodeWithAccessControl(Node):
188
188
  return masked_data
189
189
 
190
190
  def _handle_access_denied(
191
- self, decision: AccessDecision, inputs: Dict[str, Any]
191
+ self, decision: AccessDecision, inputs: dict[str, Any]
192
192
  ) -> Any:
193
193
  """
194
194
  Handle access denied scenarios.
@@ -333,6 +333,6 @@ def add_access_control(node_instance, **acl_config):
333
333
  setattr(node_instance, key, value)
334
334
 
335
335
  # Mark this node as access-controlled
336
- setattr(node_instance, "_access_controlled", True)
336
+ node_instance._access_controlled = True
337
337
 
338
338
  return node_instance
@@ -52,8 +52,9 @@ import inspect
52
52
  import logging
53
53
  import resource
54
54
  import traceback
55
+ from collections.abc import Callable
55
56
  from pathlib import Path
56
- from typing import Any, Callable, Dict, List, Optional, Type, Union, get_type_hints
57
+ from typing import Any, get_type_hints
57
58
 
58
59
  from kailash.nodes.base import Node, NodeMetadata, NodeParameter, register_node
59
60
  from kailash.sdk_exceptions import (
@@ -171,8 +172,8 @@ class CodeExecutor:
171
172
 
172
173
  def __init__(
173
174
  self,
174
- allowed_modules: Optional[List[str]] = None,
175
- security_config: Optional[SecurityConfig] = None,
175
+ allowed_modules: list[str] | None = None,
176
+ security_config: SecurityConfig | None = None,
176
177
  ):
177
178
  """Initialize the code executor.
178
179
 
@@ -269,7 +270,7 @@ class CodeExecutor:
269
270
  except SyntaxError as e:
270
271
  raise NodeExecutionError(f"Invalid Python syntax: {e}")
271
272
 
272
- def execute_code(self, code: str, inputs: Dict[str, Any]) -> Dict[str, Any]:
273
+ def execute_code(self, code: str, inputs: dict[str, Any]) -> dict[str, Any]:
273
274
  """Execute Python code with given inputs.
274
275
 
275
276
  Args:
@@ -350,7 +351,7 @@ class CodeExecutor:
350
351
  logger.error(error_msg)
351
352
  raise NodeExecutionError(error_msg)
352
353
 
353
- def execute_function(self, func: Callable, inputs: Dict[str, Any]) -> Any:
354
+ def execute_function(self, func: Callable, inputs: dict[str, Any]) -> Any:
354
355
  """Execute a Python function with given inputs.
355
356
 
356
357
  Args:
@@ -404,7 +405,7 @@ class FunctionWrapper:
404
405
  node = wrapper.to_node(name="dropna_processor")
405
406
  """
406
407
 
407
- def __init__(self, func: Callable, executor: Optional[CodeExecutor] = None):
408
+ def __init__(self, func: Callable, executor: CodeExecutor | None = None):
408
409
  """Initialize the function wrapper.
409
410
 
410
411
  Args:
@@ -422,7 +423,7 @@ class FunctionWrapper:
422
423
  # Handle cases where type hints can't be resolved
423
424
  self.type_hints = {}
424
425
 
425
- def get_input_types(self) -> Dict[str, Type]:
426
+ def get_input_types(self) -> dict[str, type]:
426
427
  """Extract input types from function signature.
427
428
 
428
429
  Returns:
@@ -438,7 +439,7 @@ class FunctionWrapper:
438
439
  input_types[param_name] = param_type
439
440
  return input_types
440
441
 
441
- def get_output_type(self) -> Type:
442
+ def get_output_type(self) -> type:
442
443
  """Extract output type from function signature.
443
444
 
444
445
  Returns:
@@ -446,22 +447,26 @@ class FunctionWrapper:
446
447
  """
447
448
  return self.type_hints.get("return", Any)
448
449
 
449
- def execute(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
450
+ def execute(self, inputs: dict[str, Any]) -> dict[str, Any]:
450
451
  """Execute the wrapped function."""
451
452
  result = self.executor.execute_function(self.func, inputs)
452
453
 
453
- # Wrap non-dict results in a dict
454
+ # Always wrap results in "result" key for consistent validation
455
+ # This ensures both dict and non-dict returns have the same structure
454
456
  if not isinstance(result, dict):
455
457
  result = {"result": result}
458
+ else:
459
+ # For dict results, wrap the entire dict in "result" key
460
+ result = {"result": result}
456
461
 
457
462
  return result
458
463
 
459
464
  def to_node(
460
465
  self,
461
- name: Optional[str] = None,
462
- description: Optional[str] = None,
463
- input_schema: Optional[Dict[str, "NodeParameter"]] = None,
464
- output_schema: Optional[Dict[str, "NodeParameter"]] = None,
466
+ name: str | None = None,
467
+ description: str | None = None,
468
+ input_schema: dict[str, "NodeParameter"] | None = None,
469
+ output_schema: dict[str, "NodeParameter"] | None = None,
465
470
  ) -> "PythonCodeNode":
466
471
  """Convert function to a PythonCodeNode.
467
472
 
@@ -507,9 +512,9 @@ class ClassWrapper:
507
512
 
508
513
  def __init__(
509
514
  self,
510
- cls: Type,
511
- method_name: Optional[str] = None,
512
- executor: Optional[CodeExecutor] = None,
515
+ cls: type,
516
+ method_name: str | None = None,
517
+ executor: CodeExecutor | None = None,
513
518
  ):
514
519
  """Initialize the class wrapper.
515
520
 
@@ -570,7 +575,7 @@ class ClassWrapper:
570
575
  # Handle descriptor objects like properties
571
576
  self.type_hints = {}
572
577
 
573
- def get_input_types(self) -> Dict[str, Type]:
578
+ def get_input_types(self) -> dict[str, type]:
574
579
  """Extract input types from method signature."""
575
580
  input_types = {}
576
581
  for param_name, param in self.signature.parameters.items():
@@ -582,11 +587,11 @@ class ClassWrapper:
582
587
  input_types[param_name] = param_type
583
588
  return input_types
584
589
 
585
- def get_output_type(self) -> Type:
590
+ def get_output_type(self) -> type:
586
591
  """Extract output type from method signature."""
587
592
  return self.type_hints.get("return", Any)
588
593
 
589
- def execute(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
594
+ def execute(self, inputs: dict[str, Any]) -> dict[str, Any]:
590
595
  """Execute the wrapped method."""
591
596
  # Create instance if needed
592
597
  if self.instance is None:
@@ -603,18 +608,22 @@ class ClassWrapper:
603
608
  # Execute the method
604
609
  result = self.executor.execute_function(method, inputs)
605
610
 
606
- # Wrap non-dict results in a dict
611
+ # Always wrap results in "result" key for consistent validation
612
+ # This ensures both dict and non-dict returns have the same structure
607
613
  if not isinstance(result, dict):
608
614
  result = {"result": result}
615
+ else:
616
+ # For dict results, wrap the entire dict in "result" key
617
+ result = {"result": result}
609
618
 
610
619
  return result
611
620
 
612
621
  def to_node(
613
622
  self,
614
- name: Optional[str] = None,
615
- description: Optional[str] = None,
616
- input_schema: Optional[Dict[str, "NodeParameter"]] = None,
617
- output_schema: Optional[Dict[str, "NodeParameter"]] = None,
623
+ name: str | None = None,
624
+ description: str | None = None,
625
+ input_schema: dict[str, "NodeParameter"] | None = None,
626
+ output_schema: dict[str, "NodeParameter"] | None = None,
618
627
  ) -> "PythonCodeNode":
619
628
  """Convert class to a PythonCodeNode.
620
629
 
@@ -707,15 +716,15 @@ class PythonCodeNode(Node):
707
716
  def __init__(
708
717
  self,
709
718
  name: str,
710
- code: Optional[str] = None,
711
- function: Optional[Callable] = None,
712
- class_type: Optional[Type] = None,
713
- process_method: Optional[str] = None,
714
- input_types: Optional[Dict[str, Type]] = None,
715
- output_type: Optional[Type] = None,
716
- input_schema: Optional[Dict[str, "NodeParameter"]] = None,
717
- output_schema: Optional[Dict[str, "NodeParameter"]] = None,
718
- description: Optional[str] = None,
719
+ code: str | None = None,
720
+ function: Callable | None = None,
721
+ class_type: type | None = None,
722
+ process_method: str | None = None,
723
+ input_types: dict[str, type] | None = None,
724
+ output_type: type | None = None,
725
+ input_schema: dict[str, "NodeParameter"] | None = None,
726
+ output_schema: dict[str, "NodeParameter"] | None = None,
727
+ description: str | None = None,
719
728
  **kwargs,
720
729
  ):
721
730
  """Initialize a Python code node.
@@ -784,7 +793,7 @@ class PythonCodeNode(Node):
784
793
  if not hasattr(self, "_skip_validation"):
785
794
  self._skip_validation = True
786
795
 
787
- def get_parameters(self) -> Dict[str, "NodeParameter"]:
796
+ def get_parameters(self) -> dict[str, "NodeParameter"]:
788
797
  """Define the parameters this node accepts.
789
798
 
790
799
  Returns:
@@ -841,7 +850,7 @@ class PythonCodeNode(Node):
841
850
 
842
851
  return parameters
843
852
 
844
- def validate_inputs(self, **kwargs) -> Dict[str, Any]:
853
+ def validate_inputs(self, **kwargs) -> dict[str, Any]:
845
854
  """Validate runtime inputs.
846
855
 
847
856
  For code-based nodes, we accept any inputs since the code
@@ -860,7 +869,7 @@ class PythonCodeNode(Node):
860
869
  # Otherwise use standard validation for function/class nodes
861
870
  return super().validate_inputs(**kwargs)
862
871
 
863
- def get_output_schema(self) -> Dict[str, "NodeParameter"]:
872
+ def get_output_schema(self) -> dict[str, "NodeParameter"]:
864
873
  """Define output parameters for this node.
865
874
 
866
875
  Returns:
@@ -880,7 +889,7 @@ class PythonCodeNode(Node):
880
889
  )
881
890
  }
882
891
 
883
- def run(self, **kwargs) -> Dict[str, Any]:
892
+ def run(self, **kwargs) -> dict[str, Any]:
884
893
  """Execute the node's logic.
885
894
 
886
895
  Args:
@@ -923,10 +932,10 @@ class PythonCodeNode(Node):
923
932
  def from_function(
924
933
  cls,
925
934
  func: Callable,
926
- name: Optional[str] = None,
927
- description: Optional[str] = None,
928
- input_schema: Optional[Dict[str, "NodeParameter"]] = None,
929
- output_schema: Optional[Dict[str, "NodeParameter"]] = None,
935
+ name: str | None = None,
936
+ description: str | None = None,
937
+ input_schema: dict[str, "NodeParameter"] | None = None,
938
+ output_schema: dict[str, "NodeParameter"] | None = None,
930
939
  **kwargs,
931
940
  ) -> "PythonCodeNode":
932
941
  """Create a node from a Python function.
@@ -961,12 +970,12 @@ class PythonCodeNode(Node):
961
970
  @classmethod
962
971
  def from_class(
963
972
  cls,
964
- class_type: Type,
965
- process_method: Optional[str] = None,
966
- name: Optional[str] = None,
967
- description: Optional[str] = None,
968
- input_schema: Optional[Dict[str, "NodeParameter"]] = None,
969
- output_schema: Optional[Dict[str, "NodeParameter"]] = None,
973
+ class_type: type,
974
+ process_method: str | None = None,
975
+ name: str | None = None,
976
+ description: str | None = None,
977
+ input_schema: dict[str, "NodeParameter"] | None = None,
978
+ output_schema: dict[str, "NodeParameter"] | None = None,
970
979
  **kwargs,
971
980
  ) -> "PythonCodeNode":
972
981
  """Create a node from a Python class.
@@ -1003,13 +1012,13 @@ class PythonCodeNode(Node):
1003
1012
  @classmethod
1004
1013
  def from_file(
1005
1014
  cls,
1006
- file_path: Union[str, Path],
1007
- function_name: Optional[str] = None,
1008
- class_name: Optional[str] = None,
1009
- name: Optional[str] = None,
1010
- description: Optional[str] = None,
1011
- input_schema: Optional[Dict[str, "NodeParameter"]] = None,
1012
- output_schema: Optional[Dict[str, "NodeParameter"]] = None,
1015
+ file_path: str | Path,
1016
+ function_name: str | None = None,
1017
+ class_name: str | None = None,
1018
+ name: str | None = None,
1019
+ description: str | None = None,
1020
+ input_schema: dict[str, "NodeParameter"] | None = None,
1021
+ output_schema: dict[str, "NodeParameter"] | None = None,
1013
1022
  ) -> "PythonCodeNode":
1014
1023
  """Create a node from a Python file.
1015
1024
 
@@ -1084,7 +1093,7 @@ class PythonCodeNode(Node):
1084
1093
  f"No suitable function or class found in {file_path}"
1085
1094
  )
1086
1095
 
1087
- def execute_code(self, inputs: Dict[str, Any]) -> Any:
1096
+ def execute_code(self, inputs: dict[str, Any]) -> Any:
1088
1097
  """Execute the code with given inputs.
1089
1098
 
1090
1099
  This is a convenience method that directly executes the code
@@ -1115,7 +1124,7 @@ class PythonCodeNode(Node):
1115
1124
  else:
1116
1125
  raise NodeExecutionError("No execution method available")
1117
1126
 
1118
- def get_config(self) -> Dict[str, Any]:
1127
+ def get_config(self) -> dict[str, Any]:
1119
1128
  """Get node configuration for serialization.
1120
1129
 
1121
1130
  Returns: