kailash 0.6.6__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. kailash/__init__.py +35 -5
  2. kailash/adapters/__init__.py +5 -0
  3. kailash/adapters/mcp_platform_adapter.py +273 -0
  4. kailash/channels/__init__.py +21 -0
  5. kailash/channels/api_channel.py +409 -0
  6. kailash/channels/base.py +271 -0
  7. kailash/channels/cli_channel.py +661 -0
  8. kailash/channels/event_router.py +496 -0
  9. kailash/channels/mcp_channel.py +648 -0
  10. kailash/channels/session.py +423 -0
  11. kailash/mcp_server/discovery.py +1 -1
  12. kailash/middleware/mcp/enhanced_server.py +22 -16
  13. kailash/nexus/__init__.py +21 -0
  14. kailash/nexus/factory.py +413 -0
  15. kailash/nexus/gateway.py +545 -0
  16. kailash/nodes/__init__.py +2 -0
  17. kailash/nodes/ai/iterative_llm_agent.py +988 -17
  18. kailash/nodes/ai/llm_agent.py +29 -9
  19. kailash/nodes/api/__init__.py +2 -2
  20. kailash/nodes/api/monitoring.py +1 -1
  21. kailash/nodes/base_async.py +54 -14
  22. kailash/nodes/code/async_python.py +1 -1
  23. kailash/nodes/data/bulk_operations.py +939 -0
  24. kailash/nodes/data/query_builder.py +373 -0
  25. kailash/nodes/data/query_cache.py +512 -0
  26. kailash/nodes/monitoring/__init__.py +10 -0
  27. kailash/nodes/monitoring/deadlock_detector.py +964 -0
  28. kailash/nodes/monitoring/performance_anomaly.py +1078 -0
  29. kailash/nodes/monitoring/race_condition_detector.py +1151 -0
  30. kailash/nodes/monitoring/transaction_metrics.py +790 -0
  31. kailash/nodes/monitoring/transaction_monitor.py +931 -0
  32. kailash/nodes/system/__init__.py +17 -0
  33. kailash/nodes/system/command_parser.py +820 -0
  34. kailash/nodes/transaction/__init__.py +48 -0
  35. kailash/nodes/transaction/distributed_transaction_manager.py +983 -0
  36. kailash/nodes/transaction/saga_coordinator.py +652 -0
  37. kailash/nodes/transaction/saga_state_storage.py +411 -0
  38. kailash/nodes/transaction/saga_step.py +467 -0
  39. kailash/nodes/transaction/transaction_context.py +756 -0
  40. kailash/nodes/transaction/two_phase_commit.py +978 -0
  41. kailash/nodes/transform/processors.py +17 -1
  42. kailash/nodes/validation/__init__.py +21 -0
  43. kailash/nodes/validation/test_executor.py +532 -0
  44. kailash/nodes/validation/validation_nodes.py +447 -0
  45. kailash/resources/factory.py +1 -1
  46. kailash/runtime/async_local.py +84 -21
  47. kailash/runtime/local.py +21 -2
  48. kailash/runtime/parameter_injector.py +187 -31
  49. kailash/security.py +16 -1
  50. kailash/servers/__init__.py +32 -0
  51. kailash/servers/durable_workflow_server.py +430 -0
  52. kailash/servers/enterprise_workflow_server.py +466 -0
  53. kailash/servers/gateway.py +183 -0
  54. kailash/servers/workflow_server.py +290 -0
  55. kailash/utils/data_validation.py +192 -0
  56. kailash/workflow/builder.py +291 -12
  57. kailash/workflow/validation.py +144 -8
  58. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/METADATA +1 -1
  59. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/RECORD +63 -25
  60. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/WHEEL +0 -0
  61. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/entry_points.txt +0 -0
  62. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/licenses/LICENSE +0 -0
  63. {kailash-0.6.6.dist-info → kailash-0.7.0.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,14 @@
2
2
 
3
3
  import logging
4
4
  import uuid
5
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  from kailash.sdk_exceptions import ConnectionError, WorkflowValidationError
8
8
  from kailash.workflow.graph import Workflow
9
9
 
10
+ if TYPE_CHECKING:
11
+ from kailash.nodes.base import Node
12
+
10
13
  logger = logging.getLogger(__name__)
11
14
 
12
15
 
@@ -19,22 +22,238 @@ class WorkflowBuilder:
19
22
  self.connections: list[dict[str, str]] = []
20
23
  self._metadata: dict[str, Any] = {}
21
24
 
22
- def add_node(
25
+ def add_node(self, *args, **kwargs) -> str:
26
+ """
27
+ Unified add_node method supporting multiple API patterns.
28
+
29
+ Supported patterns:
30
+ 1. add_node("NodeType", "node_id", {"param": value}) # Current/Preferred
31
+ 2. add_node("node_id", NodeClass, param=value) # Legacy fluent
32
+ 3. add_node(NodeClass, "node_id", param=value) # Alternative
33
+
34
+ Args:
35
+ *args: Positional arguments (pattern-dependent)
36
+ **kwargs: Keyword arguments for configuration
37
+
38
+ Returns:
39
+ Node ID (useful for method chaining)
40
+
41
+ Raises:
42
+ WorkflowValidationError: If node_id is already used or invalid pattern
43
+ """
44
+ # Pattern detection and routing
45
+ if len(args) == 0 and kwargs:
46
+ # Keyword-only pattern: add_node(node_type="NodeType", node_id="id", config={})
47
+ node_type = kwargs.pop("node_type", None)
48
+ node_id = kwargs.pop("node_id", None)
49
+ config = kwargs.pop("config", {})
50
+ # Any remaining kwargs are treated as config
51
+ config.update(kwargs)
52
+
53
+ if node_type is None:
54
+ raise WorkflowValidationError(
55
+ "node_type is required when using keyword arguments"
56
+ )
57
+
58
+ return self._add_node_current(node_type, node_id, config)
59
+
60
+ elif len(args) == 1:
61
+ # Single argument with possible keywords
62
+ if isinstance(args[0], str) and kwargs:
63
+ # Pattern: add_node("NodeType", node_id="id", config={})
64
+ node_type = args[0]
65
+ node_id = kwargs.pop("node_id", None)
66
+ config = kwargs.pop("config", {})
67
+ # Any remaining kwargs are treated as config
68
+ config.update(kwargs)
69
+ return self._add_node_current(node_type, node_id, config)
70
+ elif isinstance(args[0], str):
71
+ # Pattern: add_node("NodeType")
72
+ return self._add_node_current(args[0], None, {})
73
+ elif hasattr(args[0], "__name__"):
74
+ # Pattern: add_node(NodeClass)
75
+ return self._add_node_alternative(args[0], None, **kwargs)
76
+ else:
77
+ from kailash.nodes.base import Node
78
+
79
+ if isinstance(args[0], Node):
80
+ # Pattern: add_node(node_instance)
81
+ return self._add_node_instance(args[0], None)
82
+
83
+ elif len(args) == 3 and isinstance(args[0], str) and isinstance(args[2], dict):
84
+ # Pattern 1: Current API - add_node("NodeType", "node_id", {"param": value})
85
+ return self._add_node_current(args[0], args[1], args[2])
86
+
87
+ elif len(args) >= 2 and isinstance(args[0], str):
88
+ # Pattern 2: Legacy fluent API - add_node("node_id", NodeClass, param=value)
89
+ if hasattr(args[1], "__name__") or isinstance(args[1], type):
90
+ return self._add_node_legacy_fluent(args[0], args[1], **kwargs)
91
+ elif isinstance(args[1], str):
92
+ # Two strings - assume current API: add_node("NodeType", "node_id")
93
+ config = kwargs if kwargs else (args[2] if len(args) > 2 else {})
94
+ return self._add_node_current(args[0], args[1], config)
95
+ else:
96
+ # Invalid second argument
97
+ raise WorkflowValidationError(
98
+ f"Invalid node type: {type(args[1]).__name__}. "
99
+ "Expected: str (node type name), Node class, or Node instance"
100
+ )
101
+
102
+ elif len(args) >= 2 and hasattr(args[0], "__name__"):
103
+ # Pattern 3: Alternative - add_node(NodeClass, "node_id", param=value)
104
+ # Handle both dict config and keyword args
105
+ if len(args) == 3 and isinstance(args[2], dict):
106
+ # Config provided as dict
107
+ return self._add_node_alternative(args[0], args[1], **args[2])
108
+ else:
109
+ # Config provided as kwargs
110
+ return self._add_node_alternative(args[0], args[1], **kwargs)
111
+
112
+ elif len(args) >= 2:
113
+ # Check if first arg is a Node instance
114
+ from kailash.nodes.base import Node
115
+
116
+ if isinstance(args[0], Node):
117
+ # Pattern 4: Instance - add_node(node_instance, "node_id") or add_node(node_instance, "node_id", config)
118
+ # Config is ignored for instances
119
+ return self._add_node_instance(args[0], args[1])
120
+ elif len(args) == 2:
121
+ # Invalid arguments for 2-arg call
122
+ raise WorkflowValidationError(
123
+ f"Invalid node type: {type(args[0]).__name__}. "
124
+ "Expected: str (node type name), Node class, or Node instance"
125
+ )
126
+
127
+ # For 3 or more args that don't match other patterns
128
+ # Error with helpful message
129
+ raise WorkflowValidationError(
130
+ f"Invalid add_node signature. Received {len(args)} args: {[type(arg).__name__ for arg in args]}\n"
131
+ f"Supported patterns:\n"
132
+ f" add_node('NodeType', 'node_id', {{'param': value}}) # Preferred\n"
133
+ f" add_node('node_id', NodeClass, param=value) # Legacy\n"
134
+ f" add_node(NodeClass, 'node_id', param=value) # Alternative\n"
135
+ f"Examples:\n"
136
+ f" add_node('HTTPRequestNode', 'api_call', {{'url': 'https://api.com'}})\n"
137
+ f" add_node('csv_reader', CSVReaderNode, file_path='data.csv')"
138
+ )
139
+
140
+ def _add_node_current(
141
+ self, node_type: str, node_id: str | None, config: dict[str, Any]
142
+ ) -> str:
143
+ """Handle current API pattern: add_node('NodeType', 'node_id', {'param': value})"""
144
+ return self._add_node_unified(node_type, node_id, config)
145
+
146
+ def _add_node_legacy_fluent(
147
+ self, node_id: str, node_class_or_type: Any, **config
148
+ ) -> "WorkflowBuilder":
149
+ """Handle legacy fluent API pattern: add_node('node_id', NodeClass, param=value)"""
150
+ import warnings
151
+
152
+ from kailash.nodes.base import Node
153
+
154
+ # If it's a class, validate it's a Node subclass
155
+ if isinstance(node_class_or_type, type) and not issubclass(
156
+ node_class_or_type, Node
157
+ ):
158
+ raise WorkflowValidationError(
159
+ f"Invalid node type: {node_class_or_type}. Expected a Node subclass or string."
160
+ )
161
+
162
+ warnings.warn(
163
+ f"Legacy fluent API usage detected. "
164
+ f"Migration guide:\n"
165
+ f" OLD: add_node('{node_id}', {getattr(node_class_or_type, '__name__', str(node_class_or_type))}, {list(config.keys())})\n"
166
+ f" NEW: add_node('{getattr(node_class_or_type, '__name__', str(node_class_or_type))}', '{node_id}', {config})\n"
167
+ f"Legacy support will be removed in v0.8.0",
168
+ DeprecationWarning,
169
+ stacklevel=3,
170
+ )
171
+
172
+ if hasattr(node_class_or_type, "__name__"):
173
+ node_type = node_class_or_type.__name__
174
+ else:
175
+ node_type = str(node_class_or_type)
176
+
177
+ self._add_node_unified(node_type, node_id, config)
178
+ return self # Return self for fluent chaining
179
+
180
+ def _add_node_alternative(
181
+ self, node_class: type, node_id: str | None, **config
182
+ ) -> str:
183
+ """Handle alternative pattern: add_node(NodeClass, 'node_id', param=value)"""
184
+ import warnings
185
+
186
+ from kailash.nodes.base import Node
187
+
188
+ # Validate that node_class is actually a Node subclass
189
+ if not isinstance(node_class, type) or not issubclass(node_class, Node):
190
+ raise WorkflowValidationError(
191
+ f"Invalid node type: {node_class}. Expected a Node subclass."
192
+ )
193
+
194
+ # Generate ID if not provided
195
+ if node_id is None:
196
+ node_id = f"node_{uuid.uuid4().hex[:8]}"
197
+
198
+ warnings.warn(
199
+ f"Alternative API usage detected. Consider using preferred pattern:\n"
200
+ f" CURRENT: add_node({node_class.__name__}, '{node_id}', {list(config.keys())})\n"
201
+ f" PREFERRED: add_node('{node_class.__name__}', '{node_id}', {config})",
202
+ UserWarning,
203
+ stacklevel=3,
204
+ )
205
+
206
+ # Store the class reference along with the type name
207
+ self.nodes[node_id] = {
208
+ "type": node_class.__name__,
209
+ "config": config,
210
+ "class": node_class,
211
+ }
212
+ logger.info(f"Added node '{node_id}' of type '{node_class.__name__}'")
213
+ return node_id
214
+
215
+ def _add_node_instance(self, node_instance: "Node", node_id: str | None) -> str:
216
+ """Handle instance pattern: add_node(node_instance, 'node_id')"""
217
+ import warnings
218
+
219
+ # Generate ID if not provided
220
+ if node_id is None:
221
+ node_id = f"node_{uuid.uuid4().hex[:8]}"
222
+
223
+ warnings.warn(
224
+ f"Instance-based API usage detected. Consider using preferred pattern:\n"
225
+ f" CURRENT: add_node(<instance>, '{node_id}')\n"
226
+ f" PREFERRED: add_node('{node_instance.__class__.__name__}', '{node_id}', {{'param': value}})",
227
+ UserWarning,
228
+ stacklevel=3,
229
+ )
230
+
231
+ # Store the instance
232
+ self.nodes[node_id] = {
233
+ "instance": node_instance,
234
+ "type": node_instance.__class__.__name__,
235
+ }
236
+ logger.info(
237
+ f"Added node '{node_id}' with instance of type '{node_instance.__class__.__name__}'"
238
+ )
239
+ return node_id
240
+
241
+ def _add_node_unified(
23
242
  self,
24
- node_type: str | type | Any,
243
+ node_type: str,
25
244
  node_id: str | None = None,
26
245
  config: dict[str, Any] | None = None,
27
246
  ) -> str:
28
247
  """
29
- Add a node to the workflow.
248
+ Unified implementation for all add_node patterns.
30
249
 
31
250
  Args:
32
- node_type: Node type name (string), Node class, or Node instance
251
+ node_type: Node type name (string)
33
252
  node_id: Unique identifier for this node (auto-generated if not provided)
34
- config: Configuration for the node (ignored if node_type is an instance)
253
+ config: Configuration for the node
35
254
 
36
255
  Returns:
37
- Node ID (useful for method chaining)
256
+ Node ID
38
257
 
39
258
  Raises:
40
259
  WorkflowValidationError: If node_id is already used
@@ -80,6 +299,39 @@ class WorkflowBuilder:
80
299
  logger.info(f"Added node '{node_id}' of type '{type_name}'")
81
300
  return node_id
82
301
 
302
+ # Fluent API methods for backward compatibility
303
+ def add_node_fluent(
304
+ self, node_id: str, node_class_or_type: Any, **config
305
+ ) -> "WorkflowBuilder":
306
+ """
307
+ DEPRECATED: Fluent API for backward compatibility.
308
+ Use add_node(node_type, node_id, config) instead.
309
+
310
+ Args:
311
+ node_id: Node identifier
312
+ node_class_or_type: Node class or type
313
+ **config: Node configuration as keyword arguments
314
+
315
+ Returns:
316
+ Self for method chaining
317
+ """
318
+ import warnings
319
+
320
+ warnings.warn(
321
+ "Fluent API is deprecated. Use add_node(node_type, node_id, config) instead.",
322
+ DeprecationWarning,
323
+ stacklevel=2,
324
+ )
325
+
326
+ if hasattr(node_class_or_type, "__name__"):
327
+ # Node class
328
+ self.add_node(node_class_or_type.__name__, node_id, config)
329
+ else:
330
+ # Assume string type
331
+ self.add_node(str(node_class_or_type), node_id, config)
332
+
333
+ return self
334
+
83
335
  def add_node_instance(self, node_instance: Any, node_id: str | None = None) -> str:
84
336
  """
85
337
  Add a node instance to the workflow.
@@ -124,7 +376,7 @@ class WorkflowBuilder:
124
376
 
125
377
  def add_connection(
126
378
  self, from_node: str, from_output: str, to_node: str, to_input: str
127
- ) -> None:
379
+ ) -> "WorkflowBuilder":
128
380
  """
129
381
  Connect two nodes in the workflow.
130
382
 
@@ -161,6 +413,7 @@ class WorkflowBuilder:
161
413
  self.connections.append(connection)
162
414
 
163
415
  logger.info(f"Connected '{from_node}.{from_output}' to '{to_node}.{to_input}'")
416
+ return self
164
417
 
165
418
  def connect(
166
419
  self,
@@ -399,9 +652,21 @@ class WorkflowBuilder:
399
652
  # Dict format: {node_id: {type: "...", parameters: {...}}}
400
653
  for node_id, node_config in nodes_config.items():
401
654
  node_type = node_config.get("type")
402
- node_params = node_config.get(
403
- "parameters", node_config.get("config", {})
404
- )
655
+
656
+ # Handle parameter naming inconsistencies - prefer 'parameters' over 'config'
657
+ if "parameters" in node_config:
658
+ node_params = node_config["parameters"]
659
+ elif "config" in node_config:
660
+ node_params = node_config["config"]
661
+ else:
662
+ node_params = {}
663
+
664
+ # Ensure node_params is a dictionary
665
+ if not isinstance(node_params, dict):
666
+ logger.warning(
667
+ f"Node '{node_id}' parameters must be a dict, got {type(node_params)}. Using empty dict."
668
+ )
669
+ node_params = {}
405
670
 
406
671
  if not node_type:
407
672
  raise WorkflowValidationError(
@@ -414,7 +679,21 @@ class WorkflowBuilder:
414
679
  for node_config in nodes_config:
415
680
  node_id = node_config.get("id")
416
681
  node_type = node_config.get("type")
417
- node_params = node_config.get("config", {})
682
+
683
+ # Handle parameter naming inconsistencies - prefer 'parameters' over 'config'
684
+ if "parameters" in node_config:
685
+ node_params = node_config["parameters"]
686
+ elif "config" in node_config:
687
+ node_params = node_config["config"]
688
+ else:
689
+ node_params = {}
690
+
691
+ # Ensure node_params is a dictionary
692
+ if not isinstance(node_params, dict):
693
+ logger.warning(
694
+ f"Node '{node_id}' parameters must be a dict, got {type(node_params)}. Using empty dict."
695
+ )
696
+ node_params = {}
418
697
 
419
698
  if not node_id:
420
699
  raise WorkflowValidationError("Node ID is required")
@@ -390,6 +390,49 @@ class CycleLinter:
390
390
  )
391
391
  )
392
392
 
393
+ # Check for potentially problematic mappings
394
+ if source_param in [
395
+ "result",
396
+ "output",
397
+ "data",
398
+ ] and target_param in ["result", "output", "data"]:
399
+ if source_param != target_param:
400
+ self.issues.append(
401
+ ValidationIssue(
402
+ severity=IssueSeverity.INFO,
403
+ category="parameter_mapping",
404
+ code="CYC010A",
405
+ message=f"Generic parameter mapping '{source_param}' -> '{target_param}' in cycle {cycle_id}",
406
+ cycle_id=cycle_id,
407
+ suggestion="Consider using more specific parameter names for clarity",
408
+ documentation_link="guide/mistakes/063-cyclic-parameter-propagation-multi-fix.md",
409
+ )
410
+ )
411
+
412
+ # Check for dot notation in mappings
413
+ if (
414
+ "." in source_param
415
+ and target_param == source_param.split(".")[-1]
416
+ ):
417
+ # This is actually a good pattern - dot notation to specific field
418
+ pass
419
+ elif "." not in source_param and "." not in target_param:
420
+ # Simple mapping - check if it makes sense
421
+ if source_param.startswith(
422
+ "temp_"
423
+ ) or target_param.startswith("temp_"):
424
+ self.issues.append(
425
+ ValidationIssue(
426
+ severity=IssueSeverity.INFO,
427
+ category="parameter_mapping",
428
+ code="CYC010B",
429
+ message=f"Temporary parameter mapping '{source_param}' -> '{target_param}' in cycle {cycle_id}",
430
+ cycle_id=cycle_id,
431
+ suggestion="Consider using permanent parameter names for production workflows",
432
+ documentation_link="guide/mistakes/063-cyclic-parameter-propagation-multi-fix.md",
433
+ )
434
+ )
435
+
393
436
  # Check for missing parameter propagation
394
437
  if not mapping and len(cycle_nodes) > 1:
395
438
  self.issues.append(
@@ -589,20 +632,53 @@ class CycleLinter:
589
632
 
590
633
  def _has_unsafe_parameter_access(self, code: str) -> bool:
591
634
  """Check if PythonCodeNode has unsafe parameter access."""
592
- # Look for direct parameter access without try/except
635
+ import re
636
+
637
+ # Look for direct parameter access without try/except or safety checks
593
638
  lines = code.split("\n")
594
639
 
640
+ # Common parameter names that might be unsafe
641
+ unsafe_patterns = [
642
+ r"\b(data|input|params|context|kwargs|args)\[", # Direct indexing
643
+ r"\b(data|input|params|context|kwargs|args)\.", # Direct attribute access
644
+ r"\b(data|input|params|context|kwargs|args)\.get\(", # .get() without default
645
+ ]
646
+
647
+ # Safety patterns that indicate safe access
648
+ safety_patterns = [
649
+ r"try\s*:",
650
+ r"except\s*:",
651
+ r"if\s+.*\s+is\s+not\s+None\s*:",
652
+ r"if\s+.*\s+in\s+",
653
+ r"\.get\(.*,.*\)", # .get() with default value
654
+ r"isinstance\s*\(",
655
+ r"hasattr\s*\(",
656
+ ]
657
+
658
+ has_unsafe_access = False
659
+ has_safety_checks = False
660
+
595
661
  for line in lines:
596
662
  line = line.strip()
597
663
  if line and not line.startswith("#"):
598
- # Check for variable access that might be parameters
599
- if re.match(r"^[a-zA-Z_]\w*\s*=", line):
600
- var_name = line.split("=")[0].strip()
601
- # If variable is used before definition, might be parameter
602
- if not self._is_defined_before_use(var_name, code):
603
- return True
664
+ # Check for unsafe patterns
665
+ for pattern in unsafe_patterns:
666
+ if re.search(pattern, line):
667
+ has_unsafe_access = True
668
+ break
604
669
 
605
- return False
670
+ # Check for safety patterns
671
+ for pattern in safety_patterns:
672
+ if re.search(pattern, line):
673
+ has_safety_checks = True
674
+ break
675
+
676
+ # Also check for undefined variables (potential parameters)
677
+ undefined_vars = self._find_undefined_variables(code)
678
+ if undefined_vars:
679
+ has_unsafe_access = True
680
+
681
+ return has_unsafe_access and not has_safety_checks
606
682
 
607
683
  def _is_defined_before_use(self, var_name: str, code: str) -> bool:
608
684
  """Check if variable is defined before use in code."""
@@ -619,6 +695,66 @@ class CycleLinter:
619
695
 
620
696
  return True
621
697
 
698
+ def _find_undefined_variables(self, code: str) -> list[str]:
699
+ """Find variables that are used but not defined in the code."""
700
+ import re
701
+
702
+ lines = code.split("\n")
703
+ defined_vars = set()
704
+ used_vars = set()
705
+
706
+ # Built-in variables and functions that don't need definition
707
+ builtin_vars = {
708
+ "len",
709
+ "sum",
710
+ "min",
711
+ "max",
712
+ "dict",
713
+ "list",
714
+ "set",
715
+ "str",
716
+ "int",
717
+ "float",
718
+ "bool",
719
+ "sorted",
720
+ "print",
721
+ "isinstance",
722
+ "type",
723
+ "hasattr",
724
+ "getattr",
725
+ "True",
726
+ "False",
727
+ "None",
728
+ "range",
729
+ "enumerate",
730
+ "zip",
731
+ "any",
732
+ "all",
733
+ }
734
+
735
+ for line in lines:
736
+ line = line.strip()
737
+ if line and not line.startswith("#"):
738
+ # Find variable definitions
739
+ if (
740
+ "=" in line
741
+ and not line.startswith("if")
742
+ and not line.startswith("elif")
743
+ ):
744
+ var_match = re.match(r"^([a-zA-Z_]\w*)\s*=", line)
745
+ if var_match:
746
+ defined_vars.add(var_match.group(1))
747
+
748
+ # Find variable uses
749
+ variables = re.findall(r"\b([a-zA-Z_]\w*)\b", line)
750
+ for var in variables:
751
+ if var not in builtin_vars and not var.startswith("_"):
752
+ used_vars.add(var)
753
+
754
+ # Return variables that are used but not defined
755
+ undefined = used_vars - defined_vars
756
+ return list(undefined)
757
+
622
758
  def _is_valid_condition_syntax(self, condition: str) -> bool:
623
759
  """Check if convergence condition has valid Python syntax."""
624
760
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kailash
3
- Version: 0.6.6
3
+ Version: 0.7.0
4
4
  Summary: Python SDK for the Kailash container-node architecture
5
5
  Home-page: https://github.com/integrum/kailash-python-sdk
6
6
  Author: Integrum