kailash 0.8.7__py3-none-any.whl → 0.9.1__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 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.8.7: MCP Parameter Validation Tool, 100% MCP Protocol compliance with missing
7
- handlers, Phase 2 MCP subscriptions with GraphQL optimization, WebSocket compression, and
8
- Redis-backed distributed coordination.
9
- Previous v0.8.6: Enhanced parameter validation system with debugging tools.
6
+ New in v0.9.1: DataFlow PostgreSQL parameter conversion verification and comprehensive testing infrastructure.
7
+ Complete validation of parameter conversion chain from DataFlow to AsyncSQLDatabaseNode to PostgreSQL.
8
+ Previous v0.9.0: Complete migration from cycle=True to modern CycleBuilder API.
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.8.6"
51
+ __version__ = "0.9.1"
53
52
 
54
53
  __all__ = [
55
54
  # Core workflow components
@@ -493,7 +493,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
493
493
  and "RETURNING" not in query_upper
494
494
  and fetch_mode == FetchMode.ALL
495
495
  ):
496
- result = await conn.execute(query, *params)
496
+ if isinstance(params, dict):
497
+ result = await conn.execute(query, params)
498
+ else:
499
+ result = await conn.execute(query, *params)
497
500
  # asyncpg returns a string like "UPDATE 1", extract the count
498
501
  if isinstance(result, str):
499
502
  parts = result.split()
@@ -505,15 +508,24 @@ class PostgreSQLAdapter(DatabaseAdapter):
505
508
  return []
506
509
 
507
510
  if fetch_mode == FetchMode.ONE:
508
- row = await conn.fetchrow(query, *params)
511
+ if isinstance(params, dict):
512
+ row = await conn.fetchrow(query, params)
513
+ else:
514
+ row = await conn.fetchrow(query, *params)
509
515
  return self._convert_row(dict(row)) if row else None
510
516
  elif fetch_mode == FetchMode.ALL:
511
- rows = await conn.fetch(query, *params)
517
+ if isinstance(params, dict):
518
+ rows = await conn.fetch(query, params)
519
+ else:
520
+ rows = await conn.fetch(query, *params)
512
521
  return [self._convert_row(dict(row)) for row in rows]
513
522
  elif fetch_mode == FetchMode.MANY:
514
523
  if not fetch_size:
515
524
  raise ValueError("fetch_size required for MANY mode")
516
- rows = await conn.fetch(query, *params)
525
+ if isinstance(params, dict):
526
+ rows = await conn.fetch(query, params)
527
+ else:
528
+ rows = await conn.fetch(query, *params)
517
529
  return [self._convert_row(dict(row)) for row in rows[:fetch_size]]
518
530
  elif fetch_mode == FetchMode.ITERATOR:
519
531
  raise NotImplementedError("Iterator mode not yet implemented")
@@ -532,7 +544,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
532
544
  and "RETURNING" not in query_upper
533
545
  and fetch_mode == FetchMode.ALL
534
546
  ):
535
- result = await conn.execute(query, *params)
547
+ if isinstance(params, dict):
548
+ result = await conn.execute(query, params)
549
+ else:
550
+ result = await conn.execute(query, *params)
536
551
  # asyncpg returns a string like "UPDATE 1", extract the count
537
552
  if isinstance(result, str):
538
553
  parts = result.split()
@@ -544,15 +559,24 @@ class PostgreSQLAdapter(DatabaseAdapter):
544
559
  return []
545
560
 
546
561
  if fetch_mode == FetchMode.ONE:
547
- row = await conn.fetchrow(query, *params)
562
+ if isinstance(params, dict):
563
+ row = await conn.fetchrow(query, params)
564
+ else:
565
+ row = await conn.fetchrow(query, *params)
548
566
  return self._convert_row(dict(row)) if row else None
549
567
  elif fetch_mode == FetchMode.ALL:
550
- rows = await conn.fetch(query, *params)
568
+ if isinstance(params, dict):
569
+ rows = await conn.fetch(query, params)
570
+ else:
571
+ rows = await conn.fetch(query, *params)
551
572
  return [self._convert_row(dict(row)) for row in rows]
552
573
  elif fetch_mode == FetchMode.MANY:
553
574
  if not fetch_size:
554
575
  raise ValueError("fetch_size required for MANY mode")
555
- rows = await conn.fetch(query, *params)
576
+ if isinstance(params, dict):
577
+ rows = await conn.fetch(query, params)
578
+ else:
579
+ rows = await conn.fetch(query, *params)
556
580
  return [self._convert_row(dict(row)) for row in rows[:fetch_size]]
557
581
  elif fetch_mode == FetchMode.ITERATOR:
558
582
  raise NotImplementedError("Iterator mode not yet implemented")
@@ -85,10 +85,14 @@ class SwitchNode(Node):
85
85
  ... operator="==",
86
86
  ... value=True
87
87
  ... ))
88
- >>> workflow.connect("convergence", "switch")
89
- >>> workflow.connect("switch", "processor",
90
- ... condition="false_output", cycle=True)
91
- >>> workflow.connect("switch", "output", condition="true_output")
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kailash
3
- Version: 0.8.7
3
+ Version: 0.9.1
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
@@ -1,4 +1,4 @@
1
- kailash/__init__.py,sha256=vkTeuWU3LgYpULDOM3noaqUIq9a8fd7tFINoWsPkSlY,2582
1
+ kailash/__init__.py,sha256=idMOMtCtjj5VGvHOiFbLk1lz383ZHxbJN_bYrcmoWRU,2583
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
@@ -201,7 +201,7 @@ kailash/nodes/compliance/data_retention.py,sha256=90bH_eGwlcDzUdklAJeXQM-RcuLUGQ
201
201
  kailash/nodes/compliance/gdpr.py,sha256=ZMoHZjAo4QtGwtFCzGMrAUBFV3TbZOnJ5DZGZS87Bas,70548
202
202
  kailash/nodes/data/__init__.py,sha256=f0h4ysvXxlyFcNJLvDyXrgJ0ixwDF1cS0pJ2QNPakhg,5213
203
203
  kailash/nodes/data/async_connection.py,sha256=wfArHs9svU48bxGZIiixSV2YVn9cukNgEjagwTRu6J4,17250
204
- kailash/nodes/data/async_sql.py,sha256=p1wpT-3vYF6P5puQR8_IfPfe8rIJaa0O7YWjfKwAMM0,104101
204
+ kailash/nodes/data/async_sql.py,sha256=yKwX4_gFc_Qi6sCF134XCgtERyIhAZKK7DomYSZG3bo,105201
205
205
  kailash/nodes/data/async_vector.py,sha256=HtwQLO25IXu8Vq80qzU8rMkUAKPQ2qM0x8YxjXHlygU,21005
206
206
  kailash/nodes/data/bulk_operations.py,sha256=WVopmosVkIlweFxVt3boLdCPc93EqpYyQ1Ez9mCIt0c,34453
207
207
  kailash/nodes/data/directory.py,sha256=fbfLqD_ijRubk-4xew3604QntPsyDxqaF4k6TpfyjDg,9923
@@ -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=dC594biBTdiw3vbyeu3TgPZs7UYAfFjbXGwcYbN_5ec,28722
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=lDE36rIL1Pl1SjDkDStWMlF3qeaZRQzGugkWtcoMQRc,66013
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=0IlmghTrwYb3ivqP975DprP3aj45-8_Sn5Wq9tEutG0,43888
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.8.7.dist-info/licenses/LICENSE,sha256=Axe6g7bTrJkToK9h9j2SpRUKKNaDZDCo2lQ2zPxCE6s,1065
399
- kailash-0.8.7.dist-info/METADATA,sha256=fXme8BDd_CLjVymPEn_n0Dau5KEPyHRptQwKtvHoFhQ,21733
400
- kailash-0.8.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
401
- kailash-0.8.7.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
402
- kailash-0.8.7.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
403
- kailash-0.8.7.dist-info/RECORD,,
398
+ kailash-0.9.1.dist-info/licenses/LICENSE,sha256=Axe6g7bTrJkToK9h9j2SpRUKKNaDZDCo2lQ2zPxCE6s,1065
399
+ kailash-0.9.1.dist-info/METADATA,sha256=dHLC_VuvScP3WIWBPf5osJEE6OZEq-fxEV8KDNZE9G4,21733
400
+ kailash-0.9.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
401
+ kailash-0.9.1.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
402
+ kailash-0.9.1.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
403
+ kailash-0.9.1.dist-info/RECORD,,