kailash 0.1.4__py3-none-any.whl → 0.2.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 +1 -1
- kailash/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +38 -0
- kailash/nodes/ai/a2a.py +1790 -0
- kailash/nodes/ai/agents.py +116 -2
- kailash/nodes/ai/ai_providers.py +206 -8
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +1623 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +116 -53
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/async_operations.py +48 -9
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +212 -27
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/processors.py +119 -4
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.4.dist-info/RECORD +0 -85
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
kailash/nodes/data/readers.py
CHANGED
@@ -33,63 +33,117 @@ import json
|
|
33
33
|
from typing import Any, Dict
|
34
34
|
|
35
35
|
from kailash.nodes.base import Node, NodeParameter, register_node
|
36
|
+
from kailash.security import safe_open, validate_file_path
|
36
37
|
|
37
38
|
|
38
39
|
@register_node()
|
39
40
|
class CSVReaderNode(Node):
|
40
|
-
"""
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
3. Loading configuration from CSV
|
62
|
-
4. Ingesting sensor data logs
|
63
|
-
|
64
|
-
Upstream Sources:
|
65
|
-
- File system paths from user input
|
66
|
-
- Output paths from previous nodes
|
67
|
-
- Configuration management systems
|
41
|
+
"""
|
42
|
+
Reads data from CSV files with automatic header detection and type inference.
|
43
|
+
|
44
|
+
This node provides comprehensive CSV file reading capabilities, handling various
|
45
|
+
formats, encodings, and edge cases. It automatically detects headers, infers data
|
46
|
+
types, and provides consistent structured output for downstream processing in
|
47
|
+
Kailash workflows.
|
48
|
+
|
49
|
+
Design Philosophy:
|
50
|
+
The CSVReaderNode embodies the principle of "data accessibility without
|
51
|
+
complexity." It abstracts the intricacies of CSV parsing while providing
|
52
|
+
flexibility for various formats. The design prioritizes memory efficiency,
|
53
|
+
automatic format detection, and consistent output structure, making it easy
|
54
|
+
to integrate diverse CSV data sources into workflows.
|
55
|
+
|
56
|
+
Upstream Dependencies:
|
57
|
+
- File system providing CSV files
|
58
|
+
- Workflow orchestrators specifying file paths
|
59
|
+
- Configuration systems providing parsing options
|
60
|
+
- Previous nodes generating CSV file paths
|
61
|
+
- User inputs defining data sources
|
68
62
|
|
69
63
|
Downstream Consumers:
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
64
|
+
- DataTransformNode: Processes tabular data
|
65
|
+
- FilterNode: Applies row/column filtering
|
66
|
+
- AggregatorNode: Summarizes data
|
67
|
+
- PythonCodeNode: Custom data processing
|
68
|
+
- WriterNodes: Exports to other formats
|
69
|
+
- Visualization nodes: Creates charts
|
70
|
+
- ML nodes: Uses as training data
|
71
|
+
|
72
|
+
Configuration:
|
73
|
+
The node supports extensive CSV parsing options:
|
74
|
+
- Delimiter detection (comma, tab, pipe, etc.)
|
75
|
+
- Header row identification
|
76
|
+
- Encoding specification (UTF-8, Latin-1, etc.)
|
77
|
+
- Quote character handling
|
78
|
+
- Skip rows/comments functionality
|
79
|
+
- Column type inference
|
80
|
+
- Missing value handling
|
81
|
+
|
82
|
+
Implementation Details:
|
83
|
+
- Uses Python's csv module for robust parsing
|
84
|
+
- Implements streaming for large files
|
85
|
+
- Automatic delimiter detection when not specified
|
86
|
+
- Header detection based on first row analysis
|
87
|
+
- Type inference for numeric/date columns
|
88
|
+
- Memory-efficient processing with generators
|
89
|
+
- Unicode normalization for consistent encoding
|
74
90
|
|
75
91
|
Error Handling:
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
92
|
+
- FileNotFoundError: Clear message with path
|
93
|
+
- PermissionError: Access rights guidance
|
94
|
+
- UnicodeDecodeError: Encoding detection hints
|
95
|
+
- csv.Error: Malformed data diagnostics
|
96
|
+
- EmptyFileError: Handles zero-byte files
|
97
|
+
- Partial read recovery for corrupted files
|
98
|
+
|
99
|
+
Side Effects:
|
100
|
+
- Reads from file system
|
101
|
+
- May consume significant memory for large files
|
102
|
+
- Creates file handles (properly closed)
|
103
|
+
- Updates internal read statistics
|
104
|
+
|
105
|
+
Examples:
|
106
|
+
>>> # Basic CSV reading with headers
|
107
|
+
>>> reader = CSVReaderNode()
|
108
|
+
>>> result = reader.run(
|
109
|
+
... file_path="customers.csv",
|
110
|
+
... headers=True
|
87
111
|
... )
|
88
|
-
>>> result
|
89
|
-
>>>
|
90
|
-
>>> #
|
91
|
-
>>> #
|
112
|
+
>>> assert isinstance(result["data"], list)
|
113
|
+
>>> assert all(isinstance(row, dict) for row in result["data"])
|
114
|
+
>>> # Example output:
|
115
|
+
>>> # result["data"] = [
|
116
|
+
>>> # {"id": "1", "name": "John Doe", "age": "30"},
|
117
|
+
>>> # {"id": "2", "name": "Jane Smith", "age": "25"}
|
92
118
|
>>> # ]
|
119
|
+
>>>
|
120
|
+
>>> # Reading with custom delimiter
|
121
|
+
>>> result = reader.run(
|
122
|
+
... file_path="data.tsv",
|
123
|
+
... delimiter="\\t",
|
124
|
+
... headers=True
|
125
|
+
... )
|
126
|
+
>>>
|
127
|
+
>>> # Reading without headers (returns list of lists)
|
128
|
+
>>> result = reader.run(
|
129
|
+
... file_path="data.csv",
|
130
|
+
... headers=False
|
131
|
+
... )
|
132
|
+
>>> assert all(isinstance(row, list) for row in result["data"])
|
133
|
+
>>>
|
134
|
+
>>> # Reading with specific encoding
|
135
|
+
>>> result = reader.run(
|
136
|
+
... file_path="european_data.csv",
|
137
|
+
... encoding="iso-8859-1",
|
138
|
+
... headers=True
|
139
|
+
... )
|
140
|
+
>>>
|
141
|
+
>>> # Handling quoted fields
|
142
|
+
>>> result = reader.run(
|
143
|
+
... file_path="complex.csv",
|
144
|
+
... headers=True,
|
145
|
+
... quotechar='"'
|
146
|
+
... )
|
93
147
|
"""
|
94
148
|
|
95
149
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
@@ -192,7 +246,7 @@ class CSVReaderNode(Node):
|
|
192
246
|
- Analyzers can process row-by-row
|
193
247
|
- data_indexed is useful for lookups and joins
|
194
248
|
"""
|
195
|
-
file_path = kwargs
|
249
|
+
file_path = kwargs.get("file_path") or self.config.get("file_path")
|
196
250
|
headers = kwargs.get("headers", True)
|
197
251
|
delimiter = kwargs.get("delimiter", ",")
|
198
252
|
index_column = kwargs.get("index_column")
|
@@ -200,7 +254,10 @@ class CSVReaderNode(Node):
|
|
200
254
|
data = []
|
201
255
|
data_indexed = {}
|
202
256
|
|
203
|
-
|
257
|
+
# Validate file path for security
|
258
|
+
validated_path = validate_file_path(file_path, operation="CSV read")
|
259
|
+
|
260
|
+
with safe_open(validated_path, "r", encoding="utf-8") as f:
|
204
261
|
reader = csv.reader(f, delimiter=delimiter)
|
205
262
|
|
206
263
|
if headers:
|
@@ -349,9 +406,12 @@ class JSONReaderNode(Node):
|
|
349
406
|
- Compatible with JSONWriter for round-trip
|
350
407
|
- Transform nodes can process nested data
|
351
408
|
"""
|
352
|
-
file_path = kwargs
|
409
|
+
file_path = kwargs.get("file_path") or self.config.get("file_path")
|
353
410
|
|
354
|
-
|
411
|
+
# Validate file path for security
|
412
|
+
validated_path = validate_file_path(file_path, operation="JSON read")
|
413
|
+
|
414
|
+
with safe_open(validated_path, "r", encoding="utf-8") as f:
|
355
415
|
data = json.load(f)
|
356
416
|
|
357
417
|
return {"data": data}
|
@@ -487,10 +547,13 @@ class TextReaderNode(Node):
|
|
487
547
|
- Pattern nodes can search content
|
488
548
|
- Writers can save processed text
|
489
549
|
"""
|
490
|
-
file_path = kwargs
|
550
|
+
file_path = kwargs.get("file_path") or self.config.get("file_path")
|
491
551
|
encoding = kwargs.get("encoding", "utf-8")
|
492
552
|
|
493
|
-
|
553
|
+
# Validate file path for security
|
554
|
+
validated_path = validate_file_path(file_path, operation="text read")
|
555
|
+
|
556
|
+
with safe_open(validated_path, "r", encoding=encoding) as f:
|
494
557
|
text = f.read()
|
495
558
|
|
496
559
|
return {"text": text}
|
kailash/nodes/data/writers.py
CHANGED
@@ -34,6 +34,7 @@ import json
|
|
34
34
|
from typing import Any, Dict
|
35
35
|
|
36
36
|
from kailash.nodes.base import Node, NodeParameter, register_node
|
37
|
+
from kailash.security import safe_open, validate_file_path
|
37
38
|
|
38
39
|
|
39
40
|
@register_node()
|
@@ -190,7 +191,7 @@ class CSVWriterNode(Node):
|
|
190
191
|
- External tools can process output
|
191
192
|
- Metrics available for monitoring
|
192
193
|
"""
|
193
|
-
file_path = kwargs
|
194
|
+
file_path = kwargs.get("file_path") or self.config.get("file_path")
|
194
195
|
data = kwargs["data"]
|
195
196
|
headers = kwargs.get("headers")
|
196
197
|
delimiter = kwargs.get("delimiter", ",")
|
@@ -198,7 +199,10 @@ class CSVWriterNode(Node):
|
|
198
199
|
if not data:
|
199
200
|
return {"rows_written": 0}
|
200
201
|
|
201
|
-
|
202
|
+
# Validate file path for security
|
203
|
+
validated_path = validate_file_path(file_path, operation="CSV write")
|
204
|
+
|
205
|
+
with safe_open(validated_path, "w", newline="", encoding="utf-8") as f:
|
202
206
|
if isinstance(data[0], dict):
|
203
207
|
# Writing dictionaries
|
204
208
|
if not headers:
|
@@ -357,11 +361,14 @@ class JSONWriterNode(Node):
|
|
357
361
|
- Version control can track
|
358
362
|
- APIs can import data
|
359
363
|
"""
|
360
|
-
file_path = kwargs
|
364
|
+
file_path = kwargs.get("file_path") or self.config.get("file_path")
|
361
365
|
data = kwargs["data"]
|
362
366
|
indent = kwargs.get("indent", 2)
|
363
367
|
|
364
|
-
|
368
|
+
# Validate file path for security
|
369
|
+
validated_path = validate_file_path(file_path, operation="JSON write")
|
370
|
+
|
371
|
+
with safe_open(validated_path, "w", encoding="utf-8") as f:
|
365
372
|
json.dump(data, f, indent=indent, ensure_ascii=False)
|
366
373
|
|
367
374
|
return {"file_path": file_path}
|
@@ -517,13 +524,16 @@ class TextWriterNode(Node):
|
|
517
524
|
- Log analyzers can process
|
518
525
|
- Metrics available for monitoring
|
519
526
|
"""
|
520
|
-
file_path = kwargs
|
527
|
+
file_path = kwargs.get("file_path") or self.config.get("file_path")
|
521
528
|
text = kwargs["text"]
|
522
529
|
encoding = kwargs.get("encoding", "utf-8")
|
523
530
|
append = kwargs.get("append", False)
|
524
531
|
|
525
532
|
mode = "a" if append else "w"
|
526
|
-
|
533
|
+
# Validate file path for security
|
534
|
+
validated_path = validate_file_path(file_path, operation="text write")
|
535
|
+
|
536
|
+
with safe_open(validated_path, mode, encoding=encoding) as f:
|
527
537
|
f.write(text)
|
528
538
|
|
529
539
|
return {"file_path": file_path, "bytes_written": len(text.encode(encoding))}
|
kailash/nodes/logic/__init__.py
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
"""Logic operation nodes for the Kailash SDK."""
|
2
2
|
|
3
3
|
from kailash.nodes.logic.async_operations import AsyncMergeNode, AsyncSwitchNode
|
4
|
+
from kailash.nodes.logic.convergence import (
|
5
|
+
ConvergenceCheckerNode,
|
6
|
+
MultiCriteriaConvergenceNode,
|
7
|
+
)
|
8
|
+
from kailash.nodes.logic.loop import LoopNode
|
4
9
|
from kailash.nodes.logic.operations import MergeNode, SwitchNode
|
5
10
|
from kailash.nodes.logic.workflow import WorkflowNode
|
6
11
|
|
@@ -10,4 +15,7 @@ __all__ = [
|
|
10
15
|
"AsyncSwitchNode",
|
11
16
|
"AsyncMergeNode",
|
12
17
|
"WorkflowNode",
|
18
|
+
"LoopNode",
|
19
|
+
"ConvergenceCheckerNode",
|
20
|
+
"MultiCriteriaConvergenceNode",
|
13
21
|
]
|
@@ -32,15 +32,25 @@ class AsyncMergeNode(AsyncNode):
|
|
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 AsyncMergeNode
|
37
|
-
async_merge = AsyncMergeNode(merge_type="merge_dict", key="id")
|
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]:
|
@@ -364,6 +374,35 @@ class AsyncSwitchNode(AsyncNode):
|
|
364
374
|
|
365
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]:
|