kailash 0.8.7__py3-none-any.whl → 0.9.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.
- kailash/__init__.py +4 -5
- kailash/nodes/logic/operations.py +8 -4
- kailash/runtime/local.py +102 -0
- kailash/workflow/cyclic_runner.py +68 -0
- {kailash-0.8.7.dist-info → kailash-0.9.0.dist-info}/METADATA +1 -1
- {kailash-0.8.7.dist-info → kailash-0.9.0.dist-info}/RECORD +10 -10
- {kailash-0.8.7.dist-info → kailash-0.9.0.dist-info}/WHEEL +0 -0
- {kailash-0.8.7.dist-info → kailash-0.9.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.8.7.dist-info → kailash-0.9.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.8.7.dist-info → kailash-0.9.0.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
@@ -3,10 +3,9 @@
|
|
3
3
|
The Kailash SDK provides a comprehensive framework for creating nodes and workflows
|
4
4
|
that align with container-node architecture while allowing rapid prototyping.
|
5
5
|
|
6
|
-
New in v0.
|
7
|
-
|
8
|
-
|
9
|
-
Previous v0.8.6: Enhanced parameter validation system with debugging tools.
|
6
|
+
New in v0.9.0: Complete migration from cycle=True to modern CycleBuilder API across
|
7
|
+
all documentation and examples. Enhanced cyclic workflow patterns for enterprise use.
|
8
|
+
Previous v0.8.7: MCP Parameter Validation Tool, 100% MCP Protocol compliance.
|
10
9
|
"""
|
11
10
|
|
12
11
|
from kailash.nodes.base import Node, NodeMetadata, NodeParameter
|
@@ -49,7 +48,7 @@ except ImportError:
|
|
49
48
|
# For backward compatibility
|
50
49
|
WorkflowGraph = Workflow
|
51
50
|
|
52
|
-
__version__ = "0.
|
51
|
+
__version__ = "0.9.0"
|
53
52
|
|
54
53
|
__all__ = [
|
55
54
|
# Core workflow components
|
@@ -85,10 +85,14 @@ class SwitchNode(Node):
|
|
85
85
|
... operator="==",
|
86
86
|
... value=True
|
87
87
|
... ))
|
88
|
-
>>> workflow.
|
89
|
-
>>>
|
90
|
-
|
91
|
-
>>>
|
88
|
+
>>> workflow.add_connection("convergence", "result", "switch", "input_data")
|
89
|
+
>>> # Use CycleBuilder for cyclic connections
|
90
|
+
>>> cycle = workflow.create_cycle("convergence_loop")
|
91
|
+
>>> cycle.connect("switch", "false_output", "processor", "input")
|
92
|
+
>>> cycle.connect("processor", "result", "convergence", "data")
|
93
|
+
>>> cycle.max_iterations(50).build()
|
94
|
+
>>> # Non-cyclic output connection
|
95
|
+
>>> workflow.add_connection("switch", "true_output", "output", "data")
|
92
96
|
"""
|
93
97
|
|
94
98
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
kailash/runtime/local.py
CHANGED
@@ -636,6 +636,26 @@ class LocalRuntime:
|
|
636
636
|
if self.debug:
|
637
637
|
self.logger.debug(f"Node {node_id} inputs: {inputs}")
|
638
638
|
|
639
|
+
# CONDITIONAL EXECUTION: Skip nodes that only receive None inputs from conditional routing
|
640
|
+
if self._should_skip_conditional_node(workflow, node_id, inputs):
|
641
|
+
self.logger.info(
|
642
|
+
f"Skipping node {node_id} - all conditional inputs are None"
|
643
|
+
)
|
644
|
+
# Store None result to indicate the node was skipped
|
645
|
+
results[node_id] = None
|
646
|
+
node_outputs[node_id] = None
|
647
|
+
|
648
|
+
# Update task status if tracking is enabled
|
649
|
+
if task and task_manager:
|
650
|
+
task_manager.update_task_status(
|
651
|
+
task.task_id,
|
652
|
+
TaskStatus.COMPLETED,
|
653
|
+
result=None,
|
654
|
+
ended_at=datetime.now(UTC),
|
655
|
+
metadata={"skipped": True, "reason": "conditional_routing"},
|
656
|
+
)
|
657
|
+
continue
|
658
|
+
|
639
659
|
# Execute node with unified async/sync support and metrics collection
|
640
660
|
collector = MetricsCollector()
|
641
661
|
with collector.collect(node_id=node_id) as metrics_context:
|
@@ -1123,6 +1143,88 @@ class LocalRuntime:
|
|
1123
1143
|
metrics_collector = get_metrics_collector()
|
1124
1144
|
metrics_collector.reset_metrics()
|
1125
1145
|
|
1146
|
+
def _should_skip_conditional_node(
|
1147
|
+
self, workflow: Workflow, node_id: str, inputs: dict[str, Any]
|
1148
|
+
) -> bool:
|
1149
|
+
"""Determine if a node should be skipped due to conditional routing.
|
1150
|
+
|
1151
|
+
A node should be skipped if:
|
1152
|
+
1. It has incoming connections from conditional nodes (like SwitchNode)
|
1153
|
+
2. All of its connected inputs are None
|
1154
|
+
3. It has no node-level configuration parameters that would make it run independently
|
1155
|
+
|
1156
|
+
Args:
|
1157
|
+
workflow: The workflow being executed.
|
1158
|
+
node_id: Node ID to check.
|
1159
|
+
inputs: Prepared inputs for the node.
|
1160
|
+
|
1161
|
+
Returns:
|
1162
|
+
True if the node should be skipped, False otherwise.
|
1163
|
+
"""
|
1164
|
+
# Get all incoming edges for this node
|
1165
|
+
incoming_edges = list(workflow.graph.in_edges(node_id, data=True))
|
1166
|
+
|
1167
|
+
# If the node has no incoming connections, don't skip it
|
1168
|
+
# (it might be a source node or have configuration parameters)
|
1169
|
+
if not incoming_edges:
|
1170
|
+
return False
|
1171
|
+
|
1172
|
+
# Check if any incoming edges are from conditional nodes
|
1173
|
+
has_conditional_inputs = False
|
1174
|
+
for source_node_id, _, edge_data in incoming_edges:
|
1175
|
+
source_node = workflow._node_instances.get(source_node_id)
|
1176
|
+
if source_node and source_node.__class__.__name__ in ["SwitchNode"]:
|
1177
|
+
has_conditional_inputs = True
|
1178
|
+
break
|
1179
|
+
|
1180
|
+
# If no conditional inputs, don't skip
|
1181
|
+
if not has_conditional_inputs:
|
1182
|
+
return False
|
1183
|
+
|
1184
|
+
# Get the node instance to check for configuration parameters
|
1185
|
+
node_instance = workflow._node_instances.get(node_id)
|
1186
|
+
if not node_instance:
|
1187
|
+
return False
|
1188
|
+
|
1189
|
+
# Check if the node has configuration parameters that would make it run independently
|
1190
|
+
# (excluding standard parameters and None values)
|
1191
|
+
node_config = getattr(node_instance, "config", {})
|
1192
|
+
significant_config = {
|
1193
|
+
k: v
|
1194
|
+
for k, v in node_config.items()
|
1195
|
+
if k not in ["metadata", "name", "id"] and v is not None
|
1196
|
+
}
|
1197
|
+
|
1198
|
+
# If the node has significant configuration, it might still be valuable to run
|
1199
|
+
if significant_config:
|
1200
|
+
# Check if any connected inputs have actual data (not None)
|
1201
|
+
connected_inputs = {}
|
1202
|
+
for _, _, edge_data in incoming_edges:
|
1203
|
+
mapping = edge_data.get("mapping", {})
|
1204
|
+
for source_key, target_key in mapping.items():
|
1205
|
+
if target_key in inputs:
|
1206
|
+
connected_inputs[target_key] = inputs[target_key]
|
1207
|
+
|
1208
|
+
# If all connected inputs are None but node has config, still skip
|
1209
|
+
# The user can configure the node to run with default values if needed
|
1210
|
+
if all(v is None for v in connected_inputs.values()):
|
1211
|
+
return True
|
1212
|
+
|
1213
|
+
# Check if all connected inputs are None
|
1214
|
+
# This is the main condition for conditional routing
|
1215
|
+
has_non_none_input = False
|
1216
|
+
for _, _, edge_data in incoming_edges:
|
1217
|
+
mapping = edge_data.get("mapping", {})
|
1218
|
+
for source_key, target_key in mapping.items():
|
1219
|
+
if target_key in inputs and inputs[target_key] is not None:
|
1220
|
+
has_non_none_input = True
|
1221
|
+
break
|
1222
|
+
if has_non_none_input:
|
1223
|
+
break
|
1224
|
+
|
1225
|
+
# Skip the node if all connected inputs are None
|
1226
|
+
return not has_non_none_input
|
1227
|
+
|
1126
1228
|
def _should_stop_on_error(self, workflow: Workflow, node_id: str) -> bool:
|
1127
1229
|
"""Determine if execution should stop when a node fails.
|
1128
1230
|
|
@@ -220,6 +220,60 @@ class CyclicWorkflowExecutor:
|
|
220
220
|
else:
|
221
221
|
return obj
|
222
222
|
|
223
|
+
def _should_skip_conditional_node_cyclic(
|
224
|
+
self, workflow: Workflow, node_id: str, merged_inputs: dict[str, Any]
|
225
|
+
) -> bool:
|
226
|
+
"""Determine if a node should be skipped due to conditional routing in cyclic execution.
|
227
|
+
|
228
|
+
This is similar to LocalRuntime._should_skip_conditional_node but adapted for cyclic execution.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
workflow: The workflow being executed.
|
232
|
+
node_id: Node ID to check.
|
233
|
+
merged_inputs: Merged inputs for the node.
|
234
|
+
|
235
|
+
Returns:
|
236
|
+
True if the node should be skipped, False otherwise.
|
237
|
+
"""
|
238
|
+
# Get all incoming edges for this node
|
239
|
+
incoming_edges = list(workflow.graph.in_edges(node_id, data=True))
|
240
|
+
|
241
|
+
# If the node has no incoming connections, don't skip it
|
242
|
+
if not incoming_edges:
|
243
|
+
return False
|
244
|
+
|
245
|
+
# Check if any incoming edges are from conditional nodes
|
246
|
+
has_conditional_inputs = False
|
247
|
+
for source_node_id, _, edge_data in incoming_edges:
|
248
|
+
try:
|
249
|
+
source_node = workflow.get_node(source_node_id)
|
250
|
+
if source_node and source_node.__class__.__name__ in ["SwitchNode"]:
|
251
|
+
has_conditional_inputs = True
|
252
|
+
break
|
253
|
+
except:
|
254
|
+
continue
|
255
|
+
|
256
|
+
# If no conditional inputs, don't skip
|
257
|
+
if not has_conditional_inputs:
|
258
|
+
return False
|
259
|
+
|
260
|
+
# Check if all connected inputs are None
|
261
|
+
has_non_none_input = False
|
262
|
+
for _, _, edge_data in incoming_edges:
|
263
|
+
mapping = edge_data.get("mapping", {})
|
264
|
+
for source_key, target_key in mapping.items():
|
265
|
+
if (
|
266
|
+
target_key in merged_inputs
|
267
|
+
and merged_inputs[target_key] is not None
|
268
|
+
):
|
269
|
+
has_non_none_input = True
|
270
|
+
break
|
271
|
+
if has_non_none_input:
|
272
|
+
break
|
273
|
+
|
274
|
+
# Skip the node if all connected inputs are None
|
275
|
+
return not has_non_none_input
|
276
|
+
|
223
277
|
def _execute_with_cycles(
|
224
278
|
self,
|
225
279
|
workflow: Workflow,
|
@@ -863,6 +917,20 @@ class CyclicWorkflowExecutor:
|
|
863
917
|
except Exception as e:
|
864
918
|
logger.warning(f"Failed to create task for node '{node_id}': {e}")
|
865
919
|
|
920
|
+
# CONDITIONAL EXECUTION: Skip nodes that only receive None inputs from conditional routing
|
921
|
+
if self._should_skip_conditional_node_cyclic(workflow, node_id, merged_inputs):
|
922
|
+
logger.info(f"Skipping node {node_id} - all conditional inputs are None")
|
923
|
+
# Store None result to indicate the node was skipped
|
924
|
+
if task and task_manager:
|
925
|
+
task_manager.update_task_status(
|
926
|
+
task.task_id,
|
927
|
+
TaskStatus.COMPLETED,
|
928
|
+
result=None,
|
929
|
+
ended_at=datetime.now(UTC),
|
930
|
+
metadata={"skipped": True, "reason": "conditional_routing"},
|
931
|
+
)
|
932
|
+
return None
|
933
|
+
|
866
934
|
# Execute node with metrics collection
|
867
935
|
collector = MetricsCollector()
|
868
936
|
logger.debug(
|
@@ -1,4 +1,4 @@
|
|
1
|
-
kailash/__init__.py,sha256=
|
1
|
+
kailash/__init__.py,sha256=c9Upj65mN4RX9QCF8NliTUtNaZJJW8jiJqBvR4c6vmA,2537
|
2
2
|
kailash/__main__.py,sha256=vr7TVE5o16V6LsTmRFKG6RDKUXHpIWYdZ6Dok2HkHnI,198
|
3
3
|
kailash/access_control.py,sha256=MjKtkoQ2sg1Mgfe7ovGxVwhAbpJKvaepPWr8dxOueMA,26058
|
4
4
|
kailash/access_control_abac.py,sha256=FPfa_8PuDP3AxTjdWfiH3ntwWO8NodA0py9W8SE5dno,30263
|
@@ -248,7 +248,7 @@ kailash/nodes/logic/__init__.py,sha256=JKGFXwBDfY3s1MWQkx3ivdvCMm3b3HIXCn-wH9uMo
|
|
248
248
|
kailash/nodes/logic/async_operations.py,sha256=bpCc-t5uKi4DVJiVdXUcIZ6BWJOLsxQu-4UCEnwpw6M,27509
|
249
249
|
kailash/nodes/logic/convergence.py,sha256=ooNMdEJs8B0SeMQrzU1Uo_iPiuPXi8j-9IyRDSl9sYM,25080
|
250
250
|
kailash/nodes/logic/loop.py,sha256=34hnrcfeigcpsVcomsd-ZLE2x7f3irAd_-Q89vZzW9w,5756
|
251
|
-
kailash/nodes/logic/operations.py,sha256=
|
251
|
+
kailash/nodes/logic/operations.py,sha256=eJpTCWXqOIyP7jc057CXkhW2CBpTIVZXGkV74LUgza4,29002
|
252
252
|
kailash/nodes/logic/workflow.py,sha256=p2ED6tOWGVC50iNyUInSpJI41eBXmSF8Tb_w0h7NeD0,17136
|
253
253
|
kailash/nodes/mixins/__init__.py,sha256=0WYfu5kj-lHbFwP9g5vmlbsG8UzvI-vhOyHMEUzXbz4,558
|
254
254
|
kailash/nodes/mixins/event_emitter.py,sha256=xTeNrBWmuWIf8qYA5DZekymjjrTAD1sboW9dKbAP40w,7492
|
@@ -317,7 +317,7 @@ kailash/runtime/__init__.py,sha256=CvU-qBMESYYISqFOlYlLsYJrXJu0Gqr4x6yr4Ob_Rng,2
|
|
317
317
|
kailash/runtime/access_controlled.py,sha256=HtNJZylaB-2FuPsfEOfQ-4ny4HzwJfHaHNMu2xS1Nzs,17324
|
318
318
|
kailash/runtime/async_local.py,sha256=sYNggSU0R-oo8cCvU5ayodDBqASzUhxu994ZvZxDSC0,34010
|
319
319
|
kailash/runtime/docker.py,sha256=sZknVl1PCGfAZeyc0-exTuKlllSyjYlFIgJoiB3CRNs,23500
|
320
|
-
kailash/runtime/local.py,sha256=
|
320
|
+
kailash/runtime/local.py,sha256=gWHuSgbPWqIHYvirRo80LV8RjDvOR2PQoUs737_pAh0,70485
|
321
321
|
kailash/runtime/parallel.py,sha256=mz_wPD13-YVc3Q_8HkOs4nPQPdTjnjCcnRL7ZRM70lo,21070
|
322
322
|
kailash/runtime/parallel_cyclic.py,sha256=yANZHnePjhCPuCFbq3lFQA1K6jbCv5Of5-vIKbCsmZk,19863
|
323
323
|
kailash/runtime/parameter_injection.py,sha256=kG4GhmarsRr5t3VDFbc2G1HSbsZJg6UmienHCE2Ru7o,14852
|
@@ -380,7 +380,7 @@ kailash/workflow/cycle_debugger.py,sha256=eG-Q_kakqyhr1Ts-q4pRnO0EI7mVO9ao1s9WOx
|
|
380
380
|
kailash/workflow/cycle_exceptions.py,sha256=4_OLnbEXqIiXKzOc3uh8DzFik4wEHwl8bRZhY9Xhf2A,21838
|
381
381
|
kailash/workflow/cycle_profiler.py,sha256=aEWSCm0Xy15SjgLTpPooVJMzpFhtJWt4livR-3Me4N8,28547
|
382
382
|
kailash/workflow/cycle_state.py,sha256=hzRUvciRreWfS56Cf7ZLQPit_mlPTQDoNTawh8yi-2s,10747
|
383
|
-
kailash/workflow/cyclic_runner.py,sha256=
|
383
|
+
kailash/workflow/cyclic_runner.py,sha256=CFr4eucYrN_9K1XwiTE_ZmtbpdwDJv1OA7g_Pc0hEAI,46616
|
384
384
|
kailash/workflow/edge_infrastructure.py,sha256=lQDzs0-KdoCMqI4KAXAGbhHbwadM6t-ffJEWLlRuSNo,12448
|
385
385
|
kailash/workflow/graph.py,sha256=zRpGLXeuwtuxFBvE7_16c_bB9yqZirM_uwtfD1_MY4g,59272
|
386
386
|
kailash/workflow/input_handling.py,sha256=HrW--AmelYC8F18nkfmYlF_wXycA24RuNbDRjvM8rqk,6561
|
@@ -395,9 +395,9 @@ kailash/workflow/templates.py,sha256=XQMAKZXC2dlxgMMQhSEOWAF3hIbe9JJt9j_THchhAm8
|
|
395
395
|
kailash/workflow/type_inference.py,sha256=i1F7Yd_Z3elTXrthsLpqGbOnQBIVVVEjhRpI0HrIjd0,24492
|
396
396
|
kailash/workflow/validation.py,sha256=r2zApGiiG8UEn7p5Ji842l8OR1_KftzDkWc7gg0cac0,44675
|
397
397
|
kailash/workflow/visualization.py,sha256=nHBW-Ai8QBMZtn2Nf3EE1_aiMGi9S6Ui_BfpA5KbJPU,23187
|
398
|
-
kailash-0.
|
399
|
-
kailash-0.
|
400
|
-
kailash-0.
|
401
|
-
kailash-0.
|
402
|
-
kailash-0.
|
403
|
-
kailash-0.
|
398
|
+
kailash-0.9.0.dist-info/licenses/LICENSE,sha256=Axe6g7bTrJkToK9h9j2SpRUKKNaDZDCo2lQ2zPxCE6s,1065
|
399
|
+
kailash-0.9.0.dist-info/METADATA,sha256=h2b6X5_Qrk6f-iadmgTO-muDLzhiH2Huf1uCMBq1ANY,21733
|
400
|
+
kailash-0.9.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
401
|
+
kailash-0.9.0.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
|
402
|
+
kailash-0.9.0.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
|
403
|
+
kailash-0.9.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|