kailash 0.1.3__py3-none-any.whl → 0.1.5__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 +1 -1
- kailash/api/__init__.py +11 -1
- kailash/api/gateway.py +394 -0
- kailash/api/mcp_integration.py +478 -0
- kailash/api/workflow_api.py +29 -13
- kailash/nodes/ai/__init__.py +40 -4
- kailash/nodes/ai/a2a.py +1143 -0
- kailash/nodes/ai/agents.py +120 -6
- kailash/nodes/ai/ai_providers.py +224 -30
- kailash/nodes/ai/embedding_generator.py +34 -38
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2114 -0
- kailash/nodes/ai/llm_agent.py +351 -356
- kailash/nodes/ai/self_organizing.py +1624 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- kailash/nodes/base.py +60 -64
- kailash/nodes/code/python.py +61 -42
- kailash/nodes/data/__init__.py +10 -10
- kailash/nodes/data/readers.py +117 -66
- kailash/nodes/data/retrieval.py +1 -1
- kailash/nodes/data/sharepoint_graph.py +23 -25
- kailash/nodes/data/sql.py +24 -26
- kailash/nodes/data/writers.py +41 -44
- kailash/nodes/logic/__init__.py +9 -3
- kailash/nodes/logic/async_operations.py +60 -21
- kailash/nodes/logic/operations.py +43 -22
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mcp/client.py +29 -33
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/formatters.py +1 -1
- kailash/nodes/transform/processors.py +119 -4
- kailash/tracking/metrics_collector.py +6 -7
- kailash/utils/export.py +2 -2
- kailash/utils/templates.py +16 -16
- {kailash-0.1.3.dist-info → kailash-0.1.5.dist-info}/METADATA +293 -29
- {kailash-0.1.3.dist-info → kailash-0.1.5.dist-info}/RECORD +40 -35
- {kailash-0.1.3.dist-info → kailash-0.1.5.dist-info}/WHEEL +0 -0
- {kailash-0.1.3.dist-info → kailash-0.1.5.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.3.dist-info → kailash-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.3.dist-info → kailash-0.1.5.dist-info}/top_level.txt +0 -0
@@ -13,14 +13,14 @@ from kailash.nodes.base_async import AsyncNode
|
|
13
13
|
|
14
14
|
|
15
15
|
@register_node()
|
16
|
-
class
|
16
|
+
class AsyncMergeNode(AsyncNode):
|
17
17
|
"""Asynchronously merges multiple data sources.
|
18
18
|
|
19
19
|
Note: We implement run() to fulfill the Node abstract base class requirement,
|
20
20
|
but it's just a pass-through to async_run().
|
21
21
|
|
22
22
|
|
23
|
-
This node extends the standard
|
23
|
+
This node extends the standard MergeNode with asynchronous execution capabilities,
|
24
24
|
making it more efficient for:
|
25
25
|
|
26
26
|
1. Combining large datasets from parallel branches
|
@@ -28,23 +28,33 @@ class AsyncMerge(AsyncNode):
|
|
28
28
|
3. Processing streaming data in chunks
|
29
29
|
4. Aggregating results from various API calls
|
30
30
|
|
31
|
-
The merge operation supports the same types as the standard
|
31
|
+
The merge operation supports the same types as the standard MergeNode:
|
32
32
|
concat (list concatenation), zip (parallel iteration), and merge_dict
|
33
33
|
(dictionary merging with optional key-based joining).
|
34
34
|
|
35
|
-
|
36
|
-
# Create an
|
37
|
-
async_merge =
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
workflow.
|
43
|
-
workflow
|
35
|
+
Example usage:
|
36
|
+
>>> # Create an AsyncMergeNode
|
37
|
+
>>> async_merge = AsyncMergeNode(merge_type="merge_dict", key="id")
|
38
|
+
>>> async_merge.metadata.name
|
39
|
+
'AsyncMergeNode'
|
40
|
+
|
41
|
+
>>> # Using in a workflow
|
42
|
+
>>> from kailash.workflow.graph import Workflow
|
43
|
+
>>> workflow = Workflow("wf-001", "async_example")
|
44
|
+
>>> workflow.add_node("data_combine", async_merge)
|
45
|
+
>>> "data_combine" in workflow.nodes
|
46
|
+
True
|
47
|
+
|
48
|
+
>>> # Async execution with concat
|
49
|
+
>>> import asyncio
|
50
|
+
>>> async_merge = AsyncMergeNode(merge_type="concat")
|
51
|
+
>>> result = asyncio.run(async_merge.execute_async(data1=[1, 2], data2=[3, 4]))
|
52
|
+
>>> result['merged_data']
|
53
|
+
[1, 2, 3, 4]
|
44
54
|
"""
|
45
55
|
|
46
56
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
47
|
-
"""Define parameters for the
|
57
|
+
"""Define parameters for the AsyncMergeNode."""
|
48
58
|
# Reuse parameters from SyncMerge
|
49
59
|
return {
|
50
60
|
"data1": NodeParameter(
|
@@ -107,7 +117,7 @@ class AsyncMerge(AsyncNode):
|
|
107
117
|
}
|
108
118
|
|
109
119
|
def get_output_schema(self) -> Dict[str, NodeParameter]:
|
110
|
-
"""Define the output schema for
|
120
|
+
"""Define the output schema for AsyncMergeNode."""
|
111
121
|
return {
|
112
122
|
"merged_data": NodeParameter(
|
113
123
|
name="merged_data",
|
@@ -155,7 +165,7 @@ class AsyncMerge(AsyncNode):
|
|
155
165
|
|
156
166
|
# Check if we have at least one valid input
|
157
167
|
if not data_inputs:
|
158
|
-
self.logger.warning("No valid data inputs provided to
|
168
|
+
self.logger.warning("No valid data inputs provided to AsyncMergeNode")
|
159
169
|
return {"merged_data": None}
|
160
170
|
|
161
171
|
# If only one input was provided, return it directly
|
@@ -207,7 +217,7 @@ class AsyncMerge(AsyncNode):
|
|
207
217
|
# This will be properly wrapped by the execute() method
|
208
218
|
# which will call it in a sync context
|
209
219
|
raise RuntimeError(
|
210
|
-
"
|
220
|
+
"AsyncMergeNode.run() was called directly. Use execute() or execute_async() instead."
|
211
221
|
)
|
212
222
|
|
213
223
|
async def _async_concat(self, data_inputs: List[Any], chunk_size: int) -> Any:
|
@@ -349,25 +359,54 @@ class AsyncMerge(AsyncNode):
|
|
349
359
|
|
350
360
|
|
351
361
|
@register_node()
|
352
|
-
class
|
362
|
+
class AsyncSwitchNode(AsyncNode):
|
353
363
|
"""Asynchronously routes data to different outputs based on conditions.
|
354
364
|
|
355
365
|
Note: We implement run() to fulfill the Node abstract base class requirement,
|
356
366
|
but it's just a pass-through to async_run().
|
357
367
|
|
358
|
-
This node extends the standard
|
368
|
+
This node extends the standard SwitchNode with asynchronous execution capabilities,
|
359
369
|
making it more efficient for:
|
360
370
|
|
361
371
|
1. Processing conditional routing with I/O-bound condition evaluation
|
362
372
|
2. Handling large datasets that need to be routed based on complex criteria
|
363
373
|
3. Integrating with other asynchronous nodes in a workflow
|
364
374
|
|
365
|
-
The basic functionality is the same as the synchronous
|
375
|
+
The basic functionality is the same as the synchronous SwitchNode but optimized
|
366
376
|
for asynchronous execution.
|
377
|
+
|
378
|
+
Example usage:
|
379
|
+
>>> # Boolean condition routing
|
380
|
+
>>> import asyncio
|
381
|
+
>>> async_switch = AsyncSwitchNode(
|
382
|
+
... condition_field="status",
|
383
|
+
... operator="==",
|
384
|
+
... value="active"
|
385
|
+
... )
|
386
|
+
>>> result = asyncio.run(async_switch.execute_async(
|
387
|
+
... input_data={"status": "active", "data": "test"}
|
388
|
+
... ))
|
389
|
+
>>> result['true_output']
|
390
|
+
{'status': 'active', 'data': 'test'}
|
391
|
+
>>> result['false_output'] is None
|
392
|
+
True
|
393
|
+
|
394
|
+
>>> # Multi-case switching
|
395
|
+
>>> async_switch = AsyncSwitchNode(
|
396
|
+
... condition_field="priority",
|
397
|
+
... cases=["high", "medium", "low"]
|
398
|
+
... )
|
399
|
+
>>> result = asyncio.run(async_switch.execute_async(
|
400
|
+
... input_data={"priority": "high", "task": "urgent"}
|
401
|
+
... ))
|
402
|
+
>>> result['case_high']
|
403
|
+
{'priority': 'high', 'task': 'urgent'}
|
404
|
+
>>> result['default']
|
405
|
+
{'priority': 'high', 'task': 'urgent'}
|
367
406
|
"""
|
368
407
|
|
369
408
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
370
|
-
"""Define parameters for the
|
409
|
+
"""Define parameters for the AsyncSwitchNode."""
|
371
410
|
return {
|
372
411
|
"input_data": NodeParameter(
|
373
412
|
name="input_data",
|
@@ -603,7 +642,7 @@ class AsyncSwitch(AsyncNode):
|
|
603
642
|
# This will be properly wrapped by the execute() method
|
604
643
|
# which will call it in a sync context
|
605
644
|
raise RuntimeError(
|
606
|
-
"
|
645
|
+
"AsyncSwitchNode.run() was called directly. Use execute() or execute_async() instead."
|
607
646
|
)
|
608
647
|
|
609
648
|
async def _evaluate_condition(
|
@@ -11,7 +11,7 @@ from kailash.nodes.base import Node, NodeParameter, register_node
|
|
11
11
|
|
12
12
|
|
13
13
|
@register_node()
|
14
|
-
class
|
14
|
+
class SwitchNode(Node):
|
15
15
|
"""Routes data to different outputs based on conditions.
|
16
16
|
|
17
17
|
The Switch node enables conditional branching in workflows by evaluating
|
@@ -23,25 +23,21 @@ class Switch(Node):
|
|
23
23
|
3. Dynamic workflow paths based on data values
|
24
24
|
|
25
25
|
The outputs of Switch nodes are typically connected to different processing
|
26
|
-
nodes, and those branches can be rejoined later using a
|
27
|
-
|
28
|
-
Example usage
|
29
|
-
|
30
|
-
|
31
|
-
switch_node
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
workflow.connect("router", "success_handler", {"case_success": "input"})
|
42
|
-
workflow.connect("router", "warning_handler", {"case_warning": "input"})
|
43
|
-
workflow.connect("router", "error_handler", {"case_error": "input"})
|
44
|
-
workflow.connect("router", "default_handler", {"default": "input"})
|
26
|
+
nodes, and those branches can be rejoined later using a MergeNode.
|
27
|
+
|
28
|
+
Example usage:
|
29
|
+
>>> # Simple boolean condition
|
30
|
+
>>> switch_node = SwitchNode(condition_field="status", operator="==", value="success")
|
31
|
+
>>> switch_node.metadata.name
|
32
|
+
'SwitchNode'
|
33
|
+
|
34
|
+
>>> # Multi-case switching
|
35
|
+
>>> switch_node = SwitchNode(
|
36
|
+
... condition_field="status",
|
37
|
+
... cases=["success", "warning", "error"]
|
38
|
+
... )
|
39
|
+
>>> 'cases' in switch_node.get_parameters()
|
40
|
+
True
|
45
41
|
"""
|
46
42
|
|
47
43
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
@@ -360,7 +356,7 @@ class Switch(Node):
|
|
360
356
|
|
361
357
|
|
362
358
|
@register_node()
|
363
|
-
class
|
359
|
+
class MergeNode(Node):
|
364
360
|
"""Merges multiple data sources.
|
365
361
|
|
366
362
|
This node can combine data from multiple input sources in various ways,
|
@@ -368,12 +364,37 @@ class Merge(Node):
|
|
368
364
|
|
369
365
|
1. Combining results from parallel branches in a workflow
|
370
366
|
2. Joining related data sets
|
371
|
-
3. Combining outputs after conditional branching with the
|
367
|
+
3. Combining outputs after conditional branching with the SwitchNode
|
372
368
|
4. Aggregating collections of data
|
373
369
|
|
374
370
|
The merge operation is determined by the merge_type parameter, which supports
|
375
371
|
concat (list concatenation), zip (parallel iteration), and merge_dict (dictionary
|
376
372
|
merging with optional key-based joining for lists of dictionaries).
|
373
|
+
|
374
|
+
Example usage:
|
375
|
+
>>> # Simple list concatenation
|
376
|
+
>>> merge_node = MergeNode(merge_type="concat")
|
377
|
+
>>> result = merge_node.execute(data1=[1, 2], data2=[3, 4])
|
378
|
+
>>> result['merged_data']
|
379
|
+
[1, 2, 3, 4]
|
380
|
+
|
381
|
+
>>> # Dictionary merging
|
382
|
+
>>> merge_node = MergeNode(merge_type="merge_dict")
|
383
|
+
>>> result = merge_node.execute(
|
384
|
+
... data1={"a": 1, "b": 2},
|
385
|
+
... data2={"b": 3, "c": 4}
|
386
|
+
... )
|
387
|
+
>>> result['merged_data']
|
388
|
+
{'a': 1, 'b': 3, 'c': 4}
|
389
|
+
|
390
|
+
>>> # List of dicts merging by key
|
391
|
+
>>> merge_node = MergeNode(merge_type="merge_dict", key="id")
|
392
|
+
>>> result = merge_node.execute(
|
393
|
+
... data1=[{"id": 1, "name": "Alice"}],
|
394
|
+
... data2=[{"id": 1, "age": 30}]
|
395
|
+
... )
|
396
|
+
>>> result['merged_data']
|
397
|
+
[{'id': 1, 'name': 'Alice', 'age': 30}]
|
377
398
|
"""
|
378
399
|
|
379
400
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
kailash/nodes/logic/workflow.py
CHANGED
@@ -52,24 +52,32 @@ class WorkflowNode(Node):
|
|
52
52
|
- Runtime executing the inner workflow
|
53
53
|
- Results passed to subsequent nodes
|
54
54
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
55
|
+
Example usage:
|
56
|
+
>>> # Direct workflow wrapping
|
57
|
+
>>> from kailash.workflow.graph import Workflow
|
58
|
+
>>> from kailash.nodes.data.readers import CSVReaderNode
|
59
|
+
>>> inner_workflow = Workflow("wf-001", "data_processing")
|
60
|
+
>>> inner_workflow.add_node("reader", CSVReaderNode(file_path="data.csv"))
|
61
|
+
>>> node = WorkflowNode(workflow=inner_workflow)
|
62
|
+
>>> node.metadata.name
|
63
|
+
'WorkflowNode'
|
64
|
+
|
65
|
+
>>> # Get parameters from wrapped workflow
|
66
|
+
>>> params = node.get_parameters()
|
67
|
+
>>> 'reader_file_path' in params
|
68
|
+
True
|
69
|
+
>>> 'inputs' in params
|
70
|
+
True
|
71
|
+
|
72
|
+
>>> # Loading from dictionary
|
73
|
+
>>> workflow_dict = {
|
74
|
+
... "name": "simple",
|
75
|
+
... "nodes": {"node1": {"type": "CSVReaderNode", "config": {"file_path": "test.csv"}}},
|
76
|
+
... "connections": []
|
77
|
+
... }
|
78
|
+
>>> node = WorkflowNode(workflow_dict=workflow_dict)
|
79
|
+
>>> node._workflow.name
|
80
|
+
'simple'
|
73
81
|
|
74
82
|
Implementation Details:
|
75
83
|
- Parameters derived from workflow entry nodes
|
kailash/nodes/mcp/client.py
CHANGED
@@ -22,7 +22,7 @@ class MCPClient(Node):
|
|
22
22
|
- Input parameters for resource requests and tool calls
|
23
23
|
|
24
24
|
Downstream Consumers:
|
25
|
-
-
|
25
|
+
- LLMAgentNode nodes that need context from MCP servers
|
26
26
|
- Workflow nodes that orchestrate multi-step MCP interactions
|
27
27
|
- Data processing nodes that consume MCP resources
|
28
28
|
|
@@ -53,38 +53,34 @@ class MCPClient(Node):
|
|
53
53
|
- Logs connection events and errors for debugging
|
54
54
|
|
55
55
|
Examples:
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
"path": "/path/to/new_file.txt",
|
85
|
-
"content": "Hello, World!"
|
86
|
-
}
|
87
|
-
)
|
56
|
+
>>> # Connect to an MCP server and list resources
|
57
|
+
>>> client = MCPClient()
|
58
|
+
>>> result = client.run(
|
59
|
+
... server_config={
|
60
|
+
... "name": "filesystem-server",
|
61
|
+
... "command": "python",
|
62
|
+
... "args": ["-m", "mcp_filesystem"]
|
63
|
+
... },
|
64
|
+
... operation="list_resources"
|
65
|
+
... )
|
66
|
+
|
67
|
+
>>> # Fetch a specific resource
|
68
|
+
>>> resource = client.run(
|
69
|
+
... server_config=server_config,
|
70
|
+
... operation="read_resource",
|
71
|
+
... resource_uri="file:///path/to/document.txt"
|
72
|
+
... )
|
73
|
+
|
74
|
+
>>> # Call a tool on the server
|
75
|
+
>>> tool_result = client.run(
|
76
|
+
... server_config=server_config,
|
77
|
+
... operation="call_tool",
|
78
|
+
... tool_name="create_file",
|
79
|
+
... tool_arguments={
|
80
|
+
... "path": "/path/to/new_file.txt",
|
81
|
+
... "content": "Hello, World!"
|
82
|
+
... }
|
83
|
+
... )
|
88
84
|
"""
|
89
85
|
|
90
86
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
@@ -6,10 +6,17 @@ from kailash.nodes.transform.formatters import (
|
|
6
6
|
ContextFormatterNode,
|
7
7
|
QueryTextWrapperNode,
|
8
8
|
)
|
9
|
-
from kailash.nodes.transform.processors import
|
9
|
+
from kailash.nodes.transform.processors import (
|
10
|
+
DataTransformer,
|
11
|
+
Filter,
|
12
|
+
FilterNode,
|
13
|
+
Map,
|
14
|
+
Sort,
|
15
|
+
)
|
10
16
|
|
11
17
|
__all__ = [
|
12
18
|
"Filter",
|
19
|
+
"FilterNode",
|
13
20
|
"Map",
|
14
21
|
"Sort",
|
15
22
|
"DataTransformer",
|
@@ -90,7 +90,7 @@ Context:
|
|
90
90
|
|
91
91
|
Please provide a comprehensive answer based on the information provided above."""
|
92
92
|
|
93
|
-
# Create messages list for
|
93
|
+
# Create messages list for LLMAgentNode
|
94
94
|
messages = [{"role": "user", "content": prompt}]
|
95
95
|
|
96
96
|
return {"formatted_prompt": prompt, "messages": messages, "context": context}
|
@@ -7,8 +7,117 @@ from kailash.nodes.base import Node, NodeParameter, register_node
|
|
7
7
|
|
8
8
|
|
9
9
|
@register_node()
|
10
|
-
class
|
11
|
-
"""
|
10
|
+
class FilterNode(Node):
|
11
|
+
"""
|
12
|
+
Filters data based on configurable conditions and operators.
|
13
|
+
|
14
|
+
This node provides flexible data filtering capabilities for lists and collections,
|
15
|
+
supporting various comparison operators and field-based filtering for structured
|
16
|
+
data. It's designed to work seamlessly in data processing pipelines, reducing
|
17
|
+
datasets to items that match specific criteria.
|
18
|
+
|
19
|
+
Design Philosophy:
|
20
|
+
The FilterNode embodies the principle of "declarative data selection." Rather
|
21
|
+
than writing custom filtering code, users declare their filtering criteria
|
22
|
+
through simple configuration. The design supports both simple value filtering
|
23
|
+
and complex field-based filtering for dictionaries, making it versatile for
|
24
|
+
various data structures.
|
25
|
+
|
26
|
+
Upstream Dependencies:
|
27
|
+
- Data source nodes providing lists to filter
|
28
|
+
- Transform nodes producing structured data
|
29
|
+
- Aggregation nodes generating collections
|
30
|
+
- API nodes returning result sets
|
31
|
+
- File readers loading datasets
|
32
|
+
|
33
|
+
Downstream Consumers:
|
34
|
+
- Processing nodes working with filtered subsets
|
35
|
+
- Aggregation nodes summarizing filtered data
|
36
|
+
- Writer nodes exporting filtered results
|
37
|
+
- Visualization nodes displaying subsets
|
38
|
+
- Decision nodes based on filter results
|
39
|
+
|
40
|
+
Configuration:
|
41
|
+
The node supports flexible filtering options:
|
42
|
+
- Field selection for dictionary filtering
|
43
|
+
- Multiple comparison operators
|
44
|
+
- Type-aware comparisons
|
45
|
+
- Null value handling
|
46
|
+
- String contains operations
|
47
|
+
|
48
|
+
Implementation Details:
|
49
|
+
- Handles lists of any type (dicts, primitives, objects)
|
50
|
+
- Type coercion for numeric comparisons
|
51
|
+
- Null-safe operations
|
52
|
+
- String conversion for contains operator
|
53
|
+
- Preserves original data structure
|
54
|
+
- Zero-copy filtering (returns references)
|
55
|
+
|
56
|
+
Error Handling:
|
57
|
+
- Graceful handling of type mismatches
|
58
|
+
- Null value comparison logic
|
59
|
+
- Empty data returns empty result
|
60
|
+
- Invalid field names return no matches
|
61
|
+
- Operator errors fail safely
|
62
|
+
|
63
|
+
Side Effects:
|
64
|
+
- No side effects (pure function)
|
65
|
+
- Does not modify input data
|
66
|
+
- Returns new filtered list
|
67
|
+
|
68
|
+
Examples:
|
69
|
+
>>> # Filter list of numbers
|
70
|
+
>>> filter_node = FilterNode()
|
71
|
+
>>> result = filter_node.run(
|
72
|
+
... data=[1, 2, 3, 4, 5],
|
73
|
+
... operator=">",
|
74
|
+
... value=3
|
75
|
+
... )
|
76
|
+
>>> assert result["filtered_data"] == [4, 5]
|
77
|
+
>>>
|
78
|
+
>>> # Filter list of dictionaries by field
|
79
|
+
>>> users = [
|
80
|
+
... {"name": "Alice", "age": 30},
|
81
|
+
... {"name": "Bob", "age": 25},
|
82
|
+
... {"name": "Charlie", "age": 35}
|
83
|
+
... ]
|
84
|
+
>>> result = filter_node.run(
|
85
|
+
... data=users,
|
86
|
+
... field="age",
|
87
|
+
... operator=">=",
|
88
|
+
... value=30
|
89
|
+
... )
|
90
|
+
>>> assert len(result["filtered_data"]) == 2
|
91
|
+
>>> assert result["filtered_data"][0]["name"] == "Alice"
|
92
|
+
>>>
|
93
|
+
>>> # String contains filtering
|
94
|
+
>>> items = [
|
95
|
+
... {"title": "Python Programming"},
|
96
|
+
... {"title": "Java Development"},
|
97
|
+
... {"title": "Python for Data Science"}
|
98
|
+
... ]
|
99
|
+
>>> result = filter_node.run(
|
100
|
+
... data=items,
|
101
|
+
... field="title",
|
102
|
+
... operator="contains",
|
103
|
+
... value="Python"
|
104
|
+
... )
|
105
|
+
>>> assert len(result["filtered_data"]) == 2
|
106
|
+
>>>
|
107
|
+
>>> # Null value handling
|
108
|
+
>>> data_with_nulls = [
|
109
|
+
... {"value": 10},
|
110
|
+
... {"value": None},
|
111
|
+
... {"value": 20}
|
112
|
+
... ]
|
113
|
+
>>> result = filter_node.run(
|
114
|
+
... data=data_with_nulls,
|
115
|
+
... field="value",
|
116
|
+
... operator="!=",
|
117
|
+
... value=None
|
118
|
+
... )
|
119
|
+
>>> assert len(result["filtered_data"]) == 2
|
120
|
+
"""
|
12
121
|
|
13
122
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
14
123
|
return {
|
@@ -67,8 +176,10 @@ class Filter(Node):
|
|
67
176
|
try:
|
68
177
|
# Handle None values - they fail most comparisons
|
69
178
|
if item_value is None:
|
70
|
-
if operator
|
71
|
-
return
|
179
|
+
if operator == "==":
|
180
|
+
return compare_value is None
|
181
|
+
elif operator == "!=":
|
182
|
+
return compare_value is not None
|
72
183
|
else:
|
73
184
|
return False # None fails all other comparisons
|
74
185
|
|
@@ -379,3 +490,7 @@ class Sort(Node):
|
|
379
490
|
sorted_data = sorted(data, reverse=reverse)
|
380
491
|
|
381
492
|
return {"sorted_data": sorted_data}
|
493
|
+
|
494
|
+
|
495
|
+
# Backward compatibility aliases
|
496
|
+
Filter = FilterNode
|
@@ -88,13 +88,12 @@ class MetricsCollector:
|
|
88
88
|
metrics during node execution, with support for both process-level and
|
89
89
|
system-level monitoring.
|
90
90
|
|
91
|
-
Usage
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
performance_data = metrics.result()
|
91
|
+
Usage:
|
92
|
+
>>> collector = MetricsCollector()
|
93
|
+
>>> with collector.collect() as metrics:
|
94
|
+
... # Execute node code here
|
95
|
+
... pass
|
96
|
+
>>> performance_data = metrics.result()
|
98
97
|
"""
|
99
98
|
|
100
99
|
def __init__(self, sampling_interval: float = 0.1):
|
kailash/utils/export.py
CHANGED
@@ -88,8 +88,8 @@ class NodeMapper:
|
|
88
88
|
resources=ResourceSpec(cpu="100m", memory="256Mi"),
|
89
89
|
)
|
90
90
|
|
91
|
-
self.mappings["
|
92
|
-
python_node="
|
91
|
+
self.mappings["CSVReaderNode"] = ContainerMapping(
|
92
|
+
python_node="CSVReaderNode",
|
93
93
|
container_image="kailash/csv-reader:latest",
|
94
94
|
command=["python", "-m", "kailash.nodes.data.csv_reader"],
|
95
95
|
resources=ResourceSpec(cpu="100m", memory="512Mi"),
|