kailash 0.9.21__py3-none-any.whl → 0.9.22__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/tests/test_workflow_api_404.py +202 -0
- kailash/api/workflow_api.py +33 -1
- kailash/nodes/base.py +34 -5
- kailash/nodes/code/python.py +10 -4
- kailash/security.py +63 -17
- kailash/workflow/graph.py +5 -5
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/METADATA +1 -1
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/RECORD +14 -13
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/WHEEL +0 -0
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/entry_points.txt +0 -0
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/licenses/NOTICE +0 -0
- {kailash-0.9.21.dist-info → kailash-0.9.22.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
@@ -0,0 +1,202 @@
|
|
1
|
+
"""
|
2
|
+
Tests for WorkflowAPI Custom 404 Handler
|
3
|
+
|
4
|
+
Tests that WorkflowAPI provides helpful 404 error messages
|
5
|
+
with available endpoints when wrong paths are accessed.
|
6
|
+
|
7
|
+
These tests follow TDD: They will FAIL initially until the 404 handler is implemented.
|
8
|
+
This is expected behavior - we write tests FIRST, then implement.
|
9
|
+
"""
|
10
|
+
|
11
|
+
import pytest
|
12
|
+
from fastapi.testclient import TestClient
|
13
|
+
|
14
|
+
from kailash.api.workflow_api import WorkflowAPI
|
15
|
+
from kailash.workflow.builder import WorkflowBuilder
|
16
|
+
|
17
|
+
|
18
|
+
@pytest.fixture
|
19
|
+
def simple_workflow_api():
|
20
|
+
"""Simple WorkflowAPI for testing"""
|
21
|
+
workflow = WorkflowBuilder()
|
22
|
+
workflow.add_node(
|
23
|
+
"PythonCodeNode", "test", {"code": "result = {'message': 'test'}"}
|
24
|
+
)
|
25
|
+
api = WorkflowAPI(workflow.build())
|
26
|
+
return api
|
27
|
+
|
28
|
+
|
29
|
+
def test_404_returns_helpful_json(simple_workflow_api):
|
30
|
+
"""Test that 404 errors return helpful JSON with available endpoints."""
|
31
|
+
client = TestClient(simple_workflow_api.app)
|
32
|
+
|
33
|
+
# Try to access non-existent path
|
34
|
+
response = client.get("/nonexistent")
|
35
|
+
|
36
|
+
# Should return 404
|
37
|
+
assert response.status_code == 404
|
38
|
+
|
39
|
+
# Should be JSON
|
40
|
+
assert response.headers["content-type"] == "application/json"
|
41
|
+
|
42
|
+
# Should have helpful error message
|
43
|
+
data = response.json()
|
44
|
+
assert "error" in data or "detail" in data, "404 response should have error field"
|
45
|
+
|
46
|
+
# Should mention endpoints or provide helpful info
|
47
|
+
response_str = str(data).lower()
|
48
|
+
has_endpoint_info = (
|
49
|
+
"endpoint" in response_str
|
50
|
+
or "available" in response_str
|
51
|
+
or "path" in response_str
|
52
|
+
)
|
53
|
+
assert has_endpoint_info, "404 response should mention endpoints or paths"
|
54
|
+
|
55
|
+
|
56
|
+
def test_404_lists_available_endpoints(simple_workflow_api):
|
57
|
+
"""Test that 404 response includes list of available endpoints."""
|
58
|
+
client = TestClient(simple_workflow_api.app)
|
59
|
+
|
60
|
+
# Try to access non-existent path
|
61
|
+
response = client.get("/invalid_path")
|
62
|
+
|
63
|
+
assert response.status_code == 404
|
64
|
+
|
65
|
+
data = response.json()
|
66
|
+
|
67
|
+
# Should have some reference to available endpoints
|
68
|
+
response_str = str(data)
|
69
|
+
endpoints = []
|
70
|
+
|
71
|
+
# Check if standard endpoints are mentioned
|
72
|
+
if "/execute" in response_str:
|
73
|
+
endpoints.append("/execute")
|
74
|
+
if "/workflow/info" in response_str or "/info" in response_str:
|
75
|
+
endpoints.append("/workflow/info")
|
76
|
+
if "/health" in response_str:
|
77
|
+
endpoints.append("/health")
|
78
|
+
|
79
|
+
# Should mention at least 2 of the 3 standard endpoints
|
80
|
+
assert (
|
81
|
+
len(endpoints) >= 2
|
82
|
+
), f"404 response should mention available endpoints. Found: {endpoints}"
|
83
|
+
|
84
|
+
|
85
|
+
def test_404_provides_helpful_hint(simple_workflow_api):
|
86
|
+
"""Test that 404 response provides actionable hint."""
|
87
|
+
client = TestClient(simple_workflow_api.app)
|
88
|
+
|
89
|
+
# Try wrong path
|
90
|
+
response = client.get("/wrong")
|
91
|
+
|
92
|
+
assert response.status_code == 404
|
93
|
+
|
94
|
+
data = response.json()
|
95
|
+
response_str = str(data).lower()
|
96
|
+
|
97
|
+
# Should have hint, message, or suggestion
|
98
|
+
has_helpful_content = (
|
99
|
+
"hint" in response_str
|
100
|
+
or "try" in response_str
|
101
|
+
or "use" in response_str
|
102
|
+
or "most common" in response_str
|
103
|
+
or "suggestion" in response_str
|
104
|
+
or "did you mean" in response_str
|
105
|
+
)
|
106
|
+
|
107
|
+
assert (
|
108
|
+
has_helpful_content
|
109
|
+
), "404 response should provide helpful hints or suggestions"
|
110
|
+
|
111
|
+
|
112
|
+
def test_404_includes_documentation_link(simple_workflow_api):
|
113
|
+
"""Test that 404 response includes link to documentation."""
|
114
|
+
client = TestClient(simple_workflow_api.app)
|
115
|
+
|
116
|
+
response = client.get("/missing")
|
117
|
+
|
118
|
+
assert response.status_code == 404
|
119
|
+
data = response.json()
|
120
|
+
|
121
|
+
# Should have docs link or path
|
122
|
+
response_str = str(data).lower()
|
123
|
+
has_docs_reference = (
|
124
|
+
"docs" in response_str
|
125
|
+
or "documentation" in response_str
|
126
|
+
or "/docs" in response_str
|
127
|
+
)
|
128
|
+
assert has_docs_reference, "404 response should reference documentation"
|
129
|
+
|
130
|
+
|
131
|
+
def test_404_handler_for_root_path(simple_workflow_api):
|
132
|
+
"""Test 404 handler when accessing root path with wrong method."""
|
133
|
+
client = TestClient(simple_workflow_api.app)
|
134
|
+
|
135
|
+
# Try to GET root (only POST is typically defined for execute)
|
136
|
+
response = client.get("/")
|
137
|
+
|
138
|
+
# Could be 404 or 405 Method Not Allowed
|
139
|
+
assert response.status_code in [
|
140
|
+
404,
|
141
|
+
405,
|
142
|
+
], f"Expected 404 or 405, got {response.status_code}"
|
143
|
+
|
144
|
+
# If 404, should have helpful info
|
145
|
+
if response.status_code == 404:
|
146
|
+
data = response.json()
|
147
|
+
response_str = str(data).lower()
|
148
|
+
|
149
|
+
# Should mention execute or available methods
|
150
|
+
has_helpful_info = (
|
151
|
+
"execute" in response_str
|
152
|
+
or "post" in response_str
|
153
|
+
or "endpoint" in response_str
|
154
|
+
)
|
155
|
+
assert (
|
156
|
+
has_helpful_info
|
157
|
+
), "404 on root should mention execute endpoint or POST method"
|
158
|
+
|
159
|
+
|
160
|
+
def test_404_handler_preserves_fastapi_routes(simple_workflow_api):
|
161
|
+
"""Test that valid routes still work after adding 404 handler."""
|
162
|
+
client = TestClient(simple_workflow_api.app)
|
163
|
+
|
164
|
+
# Valid route should work
|
165
|
+
response = client.get("/health")
|
166
|
+
|
167
|
+
# Health endpoint should return 200
|
168
|
+
assert (
|
169
|
+
response.status_code == 200
|
170
|
+
), "Valid routes should still work after adding 404 handler"
|
171
|
+
|
172
|
+
# Should return JSON
|
173
|
+
assert response.headers["content-type"] == "application/json"
|
174
|
+
|
175
|
+
|
176
|
+
def test_404_response_format_consistency(simple_workflow_api):
|
177
|
+
"""Test that 404 responses have consistent format."""
|
178
|
+
client = TestClient(simple_workflow_api.app)
|
179
|
+
|
180
|
+
# Try multiple invalid paths
|
181
|
+
paths = ["/invalid1", "/wrong/path", "/nonexistent/endpoint"]
|
182
|
+
|
183
|
+
for path in paths:
|
184
|
+
response = client.get(path)
|
185
|
+
assert response.status_code == 404
|
186
|
+
|
187
|
+
# All should return JSON
|
188
|
+
assert (
|
189
|
+
response.headers["content-type"] == "application/json"
|
190
|
+
), f"404 response for {path} should be JSON"
|
191
|
+
|
192
|
+
# All should have error structure
|
193
|
+
data = response.json()
|
194
|
+
assert isinstance(
|
195
|
+
data, dict
|
196
|
+
), f"404 response for {path} should be a JSON object"
|
197
|
+
|
198
|
+
# Should have at least one error-related field
|
199
|
+
has_error_field = any(key in data for key in ["error", "detail", "message"])
|
200
|
+
assert (
|
201
|
+
has_error_field
|
202
|
+
), f"404 response for {path} should have error/detail/message field"
|
kailash/api/workflow_api.py
CHANGED
@@ -12,7 +12,7 @@ from typing import Any
|
|
12
12
|
|
13
13
|
import uvicorn
|
14
14
|
from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
|
15
|
-
from fastapi.responses import StreamingResponse
|
15
|
+
from fastapi.responses import JSONResponse, StreamingResponse
|
16
16
|
from pydantic import BaseModel, Field
|
17
17
|
|
18
18
|
from kailash.runtime.local import LocalRuntime
|
@@ -129,6 +129,38 @@ class WorkflowAPI:
|
|
129
129
|
def _setup_routes(self):
|
130
130
|
"""Setup API routes dynamically based on workflow."""
|
131
131
|
|
132
|
+
# Custom 404 handler for helpful error messages
|
133
|
+
@self.app.exception_handler(404)
|
134
|
+
async def custom_404_handler(request: Request, exc):
|
135
|
+
"""Provide helpful 404 error with available endpoints."""
|
136
|
+
return JSONResponse(
|
137
|
+
status_code=404,
|
138
|
+
content={
|
139
|
+
"error": "Endpoint not found",
|
140
|
+
"path": request.url.path,
|
141
|
+
"message": "The requested endpoint does not exist for this workflow.",
|
142
|
+
"available_endpoints": [
|
143
|
+
{
|
144
|
+
"method": "POST",
|
145
|
+
"path": "/execute",
|
146
|
+
"description": "Execute the workflow with input parameters",
|
147
|
+
},
|
148
|
+
{
|
149
|
+
"method": "GET",
|
150
|
+
"path": "/workflow/info",
|
151
|
+
"description": "Get workflow metadata and structure",
|
152
|
+
},
|
153
|
+
{
|
154
|
+
"method": "GET",
|
155
|
+
"path": "/health",
|
156
|
+
"description": "Check workflow API health status",
|
157
|
+
},
|
158
|
+
],
|
159
|
+
"hint": "Most common: POST to /execute endpoint with JSON body containing 'inputs' field",
|
160
|
+
"documentation": "/docs",
|
161
|
+
},
|
162
|
+
)
|
163
|
+
|
132
164
|
# Root execution endpoint (convenience for direct workflow execution)
|
133
165
|
@self.app.post("/")
|
134
166
|
async def execute_workflow_root(
|
kailash/nodes/base.py
CHANGED
@@ -202,11 +202,13 @@ class Node(ABC):
|
|
202
202
|
- Validates parameters are correctly specified
|
203
203
|
"""
|
204
204
|
try:
|
205
|
-
|
205
|
+
# Use _node_id for internal node identifier (namespace separation)
|
206
|
+
# This prevents collision with user's 'id' parameter
|
207
|
+
self._node_id = kwargs.get("_node_id", self.__class__.__name__)
|
206
208
|
self.metadata = kwargs.get(
|
207
209
|
"metadata",
|
208
210
|
NodeMetadata(
|
209
|
-
id=self.id
|
211
|
+
id=self._node_id, # NodeMetadata still uses 'id' internally
|
210
212
|
name=kwargs.get("name", self.__class__.__name__),
|
211
213
|
description=kwargs.get("description", self.__doc__ or ""),
|
212
214
|
version=kwargs.get("version", "1.0.0"),
|
@@ -214,7 +216,7 @@ class Node(ABC):
|
|
214
216
|
tags=kwargs.get("tags", set()),
|
215
217
|
),
|
216
218
|
)
|
217
|
-
self.logger = logging.getLogger(f"kailash.nodes.{self.
|
219
|
+
self.logger = logging.getLogger(f"kailash.nodes.{self._node_id}")
|
218
220
|
|
219
221
|
# Filter out internal fields from config with comprehensive parameter handling
|
220
222
|
# Get parameter definitions once and cache for both filtering and validation
|
@@ -232,11 +234,12 @@ class Node(ABC):
|
|
232
234
|
|
233
235
|
# Comprehensive parameter filtering: handle ALL potential conflicts
|
234
236
|
# Fields that are always internal (never user parameters)
|
235
|
-
always_internal = {"metadata"}
|
237
|
+
always_internal = {"metadata", "_node_id"}
|
236
238
|
|
237
239
|
# Fields that can be either internal or user parameters
|
240
|
+
# Note: 'id' removed from this list - users can now use 'id' freely
|
241
|
+
# since node identifier is now '_node_id'
|
238
242
|
potentially_user_params = {
|
239
|
-
"id",
|
240
243
|
"name",
|
241
244
|
"description",
|
242
245
|
"version",
|
@@ -344,6 +347,32 @@ class Node(ABC):
|
|
344
347
|
self._workflow_context = {}
|
345
348
|
self._workflow_context[key] = value
|
346
349
|
|
350
|
+
@property
|
351
|
+
def id(self) -> str:
|
352
|
+
"""
|
353
|
+
Backward compatibility property for node identifier.
|
354
|
+
|
355
|
+
Returns the node's identifier (_node_id). This property maintains
|
356
|
+
backward compatibility for code that accesses node.id.
|
357
|
+
|
358
|
+
The internal identifier is now _node_id to prevent namespace collision
|
359
|
+
with user's 'id' parameter.
|
360
|
+
"""
|
361
|
+
return self._node_id
|
362
|
+
|
363
|
+
@id.setter
|
364
|
+
def id(self, value: str):
|
365
|
+
"""
|
366
|
+
Setter for backward compatibility with code that sets node.id.
|
367
|
+
|
368
|
+
This allows graph.py and other code to set the node identifier
|
369
|
+
while internally using _node_id for namespace separation.
|
370
|
+
|
371
|
+
Args:
|
372
|
+
value: The node identifier to set
|
373
|
+
"""
|
374
|
+
self._node_id = value
|
375
|
+
|
347
376
|
@abstractmethod
|
348
377
|
def get_parameters(self) -> dict[str, NodeParameter]:
|
349
378
|
"""Define the parameters this node accepts.
|
kailash/nodes/code/python.py
CHANGED
@@ -412,8 +412,11 @@ class CodeExecutor:
|
|
412
412
|
# Check code safety first
|
413
413
|
is_safe, violations, imports_found = self.check_code_safety(code)
|
414
414
|
|
415
|
-
# Sanitize inputs
|
416
|
-
|
415
|
+
# Sanitize inputs with python_exec context
|
416
|
+
# Python code execution via exec() does not need shell metacharacter sanitization
|
417
|
+
sanitized_inputs = validate_node_parameters(
|
418
|
+
inputs, self.security_config, context="python_exec"
|
419
|
+
)
|
417
420
|
|
418
421
|
# Create isolated namespace
|
419
422
|
import builtins
|
@@ -572,8 +575,11 @@ class CodeExecutor:
|
|
572
575
|
Raises:
|
573
576
|
NodeExecutionError: If function execution fails
|
574
577
|
"""
|
575
|
-
# Sanitize inputs for security
|
576
|
-
|
578
|
+
# Sanitize inputs for security with python_exec context
|
579
|
+
# Python function execution does not need shell metacharacter sanitization
|
580
|
+
sanitized_inputs = validate_node_parameters(
|
581
|
+
inputs, self.security_config, context="python_exec"
|
582
|
+
)
|
577
583
|
|
578
584
|
try:
|
579
585
|
# Get function signature
|
kailash/security.py
CHANGED
@@ -475,6 +475,7 @@ def sanitize_input(
|
|
475
475
|
max_length: int = 10000,
|
476
476
|
allowed_types: list[type] | None = None,
|
477
477
|
config: SecurityConfig | None = None,
|
478
|
+
context: str = "generic",
|
478
479
|
) -> Any:
|
479
480
|
"""
|
480
481
|
Sanitize input values to prevent injection attacks.
|
@@ -484,12 +485,22 @@ def sanitize_input(
|
|
484
485
|
max_length: Maximum string length
|
485
486
|
allowed_types: List of allowed types
|
486
487
|
config: Security configuration
|
488
|
+
context: Execution context for context-aware sanitization.
|
489
|
+
- "generic": Default moderate sanitization (backward compatible)
|
490
|
+
- "python_exec": Python code execution (preserves shell metacharacters)
|
491
|
+
- "shell_exec": Shell command execution (removes all dangerous characters)
|
487
492
|
|
488
493
|
Returns:
|
489
494
|
Sanitized value
|
490
495
|
|
491
496
|
Raises:
|
492
497
|
SecurityError: If input fails validation
|
498
|
+
|
499
|
+
Note:
|
500
|
+
The context parameter allows for appropriate security measures based on
|
501
|
+
how the data will be used. Python code execution via exec() does not
|
502
|
+
need shell metacharacter sanitization since characters like $, ;, &, |
|
503
|
+
are regular Python string characters and not executed by a shell.
|
493
504
|
"""
|
494
505
|
if config is None:
|
495
506
|
config = get_security_config()
|
@@ -751,32 +762,59 @@ def sanitize_input(
|
|
751
762
|
if len(value) > max_length:
|
752
763
|
raise SecurityError(f"Input too long: {len(value)} > {max_length}")
|
753
764
|
|
754
|
-
#
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
765
|
+
# Context-aware sanitization
|
766
|
+
if context == "python_exec":
|
767
|
+
# Python execution context: Only remove XSS patterns, preserve shell metacharacters
|
768
|
+
# Python exec() does not execute shell commands, so $, ;, &, |, `, (, ) are safe
|
769
|
+
sanitized = re.sub(
|
770
|
+
r"<script.*?</script>", "", value, flags=re.IGNORECASE | re.DOTALL
|
771
|
+
)
|
772
|
+
sanitized = re.sub(r"javascript:", "", sanitized, flags=re.IGNORECASE)
|
773
|
+
# Remove only the most dangerous HTML tags for XSS prevention
|
774
|
+
sanitized = re.sub(
|
775
|
+
r"</?(?:script|iframe|object|embed).*?>",
|
776
|
+
"",
|
777
|
+
sanitized,
|
778
|
+
flags=re.IGNORECASE,
|
779
|
+
)
|
780
|
+
elif context == "shell_exec":
|
781
|
+
# Shell execution context: Remove all shell metacharacters
|
782
|
+
sanitized = re.sub(r"[<>;&|`$()]", "", value)
|
783
|
+
sanitized = re.sub(
|
784
|
+
r"<script.*?</script>", "", sanitized, flags=re.IGNORECASE | re.DOTALL
|
785
|
+
)
|
786
|
+
sanitized = re.sub(r"javascript:", "", sanitized, flags=re.IGNORECASE)
|
787
|
+
else:
|
788
|
+
# Generic context: Moderate sanitization (backward compatible)
|
789
|
+
# Remove only basic XSS patterns, preserve most characters
|
790
|
+
sanitized = re.sub(
|
791
|
+
r"<script.*?</script>", "", value, flags=re.IGNORECASE | re.DOTALL
|
792
|
+
)
|
793
|
+
sanitized = re.sub(r"javascript:", "", sanitized, flags=re.IGNORECASE)
|
794
|
+
# Remove angle brackets for basic XSS protection
|
795
|
+
sanitized = re.sub(r"[<>]", "", sanitized)
|
761
796
|
|
762
797
|
if sanitized != value and config.enable_audit_logging:
|
763
|
-
logger.warning(
|
798
|
+
logger.warning(
|
799
|
+
f"Input sanitized ({context}): {value[:50]}... -> {sanitized[:50]}..."
|
800
|
+
)
|
764
801
|
|
765
802
|
return sanitized
|
766
803
|
|
767
804
|
# Dictionary sanitization (recursive)
|
768
805
|
if isinstance(value, dict):
|
769
806
|
return {
|
770
|
-
sanitize_input(
|
771
|
-
|
772
|
-
)
|
807
|
+
sanitize_input(
|
808
|
+
k, max_length, allowed_types, config, context
|
809
|
+
): sanitize_input(v, max_length, allowed_types, config, context)
|
773
810
|
for k, v in value.items()
|
774
811
|
}
|
775
812
|
|
776
813
|
# List sanitization (recursive)
|
777
814
|
if isinstance(value, list):
|
778
815
|
return [
|
779
|
-
sanitize_input(item, max_length, allowed_types, config)
|
816
|
+
sanitize_input(item, max_length, allowed_types, config, context)
|
817
|
+
for item in value
|
780
818
|
]
|
781
819
|
|
782
820
|
return value
|
@@ -811,7 +849,9 @@ def create_secure_temp_dir(
|
|
811
849
|
|
812
850
|
|
813
851
|
def validate_node_parameters(
|
814
|
-
parameters: dict[str, Any],
|
852
|
+
parameters: dict[str, Any],
|
853
|
+
config: SecurityConfig | None = None,
|
854
|
+
context: str = "generic",
|
815
855
|
) -> dict[str, Any]:
|
816
856
|
"""
|
817
857
|
Validate and sanitize node parameters.
|
@@ -819,6 +859,10 @@ def validate_node_parameters(
|
|
819
859
|
Args:
|
820
860
|
parameters: Node parameters to validate
|
821
861
|
config: Security configuration
|
862
|
+
context: Execution context for context-aware sanitization
|
863
|
+
- "generic": Default moderate sanitization
|
864
|
+
- "python_exec": Python code execution (preserves shell metacharacters)
|
865
|
+
- "shell_exec": Shell command execution (removes all dangerous characters)
|
822
866
|
|
823
867
|
Returns:
|
824
868
|
Validated and sanitized parameters
|
@@ -833,20 +877,22 @@ def validate_node_parameters(
|
|
833
877
|
|
834
878
|
for key, value in parameters.items():
|
835
879
|
# Sanitize parameter key
|
836
|
-
clean_key = sanitize_input(key, config=config)
|
880
|
+
clean_key = sanitize_input(key, config=config, context=context)
|
837
881
|
|
838
882
|
# Special handling for file paths
|
839
883
|
if "path" in key.lower() or "file" in key.lower():
|
840
884
|
if isinstance(value, (str, Path)):
|
841
885
|
validated_value = validate_file_path(value, config, f"parameter {key}")
|
842
886
|
else:
|
843
|
-
validated_value = sanitize_input(value, config=config)
|
887
|
+
validated_value = sanitize_input(value, config=config, context=context)
|
844
888
|
else:
|
845
|
-
validated_value = sanitize_input(value, config=config)
|
889
|
+
validated_value = sanitize_input(value, config=config, context=context)
|
846
890
|
|
847
891
|
validated_params[clean_key] = validated_value
|
848
892
|
|
849
893
|
if config.enable_audit_logging:
|
850
|
-
logger.info(
|
894
|
+
logger.info(
|
895
|
+
f"Node parameters validated ({context}): {list(validated_params.keys())}"
|
896
|
+
)
|
851
897
|
|
852
898
|
return validated_params
|
kailash/workflow/graph.py
CHANGED
@@ -154,19 +154,19 @@ class Workflow:
|
|
154
154
|
|
155
155
|
try:
|
156
156
|
# Handle different constructor patterns
|
157
|
-
if "name" in params and "
|
157
|
+
if "name" in params and "_node_id" not in params:
|
158
158
|
# Node expects 'name' parameter (like PythonCodeNode)
|
159
159
|
if "name" not in config:
|
160
160
|
config = config.copy() # Don't modify original
|
161
161
|
config["name"] = node_id
|
162
162
|
return node_class(**config)
|
163
|
-
elif "
|
164
|
-
# Node expects '
|
165
|
-
return node_class(
|
163
|
+
elif "_node_id" in params:
|
164
|
+
# Node expects '_node_id' parameter (namespace-separated metadata)
|
165
|
+
return node_class(_node_id=node_id, **config)
|
166
166
|
else:
|
167
167
|
# Fallback: try both patterns
|
168
168
|
try:
|
169
|
-
return node_class(
|
169
|
+
return node_class(_node_id=node_id, **config)
|
170
170
|
except TypeError:
|
171
171
|
# Try with name parameter
|
172
172
|
config = config.copy()
|
@@ -1,10 +1,10 @@
|
|
1
|
-
kailash/__init__.py,sha256=
|
1
|
+
kailash/__init__.py,sha256=ZtUDcORH0Y5VBrpecs4mSSBKdt_N-ylzx-vJMSoj3sA,2928
|
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
|
5
5
|
kailash/manifest.py,sha256=qzOmeMGWz20Sp4IJitSH9gTVbGng7hlimc96VTW4KI8,24814
|
6
6
|
kailash/sdk_exceptions.py,sha256=MeFNmFzDzs5g9PveymivIBp1vN6PI7eenGv-Dj62Gog,10774
|
7
|
-
kailash/security.py,sha256=
|
7
|
+
kailash/security.py,sha256=O6snw6rKFWntKN_W1QqYxxfI3HHm2Qd-2VRvrC7LU6s,29634
|
8
8
|
kailash/access_control/__init__.py,sha256=ykR_zGJXQoCU_4gGNFbYzhVdUemxeAAoDQLhKIPVBGE,4018
|
9
9
|
kailash/access_control/managers.py,sha256=Vg2inaZqR2GBXsySvPZcEqQtFHgF94z7A_wUHMtA3qA,14431
|
10
10
|
kailash/access_control/rule_evaluators.py,sha256=niguhjThBjA0jIXvdKdGAXzdSM_bAd0ebphGgRrDFKU,15337
|
@@ -18,7 +18,8 @@ kailash/api/custom_nodes_secure.py,sha256=cMh1FbEtUAEoLmTb5ew_CeX5kjikBzmBsat1GO
|
|
18
18
|
kailash/api/gateway.py,sha256=BVEKyC53JRnBkOyg3YXAPGDzLm3zon0vbO1xiM6yNxU,12540
|
19
19
|
kailash/api/mcp_integration.py,sha256=xY5VjYXh4bFuNyyWBXEuTm4jjwUn8_9QxZpa4hh6a0Q,14521
|
20
20
|
kailash/api/studio.py,sha256=8Gj3R3p_-EpJerkWYLyrqVzJprKOQmTmK-1EgiLoXMc,34892
|
21
|
-
kailash/api/workflow_api.py,sha256=
|
21
|
+
kailash/api/workflow_api.py,sha256=xRbvqKH-pVbZeV2NqT3pfP1ykXqX38BvT5uheeyw3Hg,15805
|
22
|
+
kailash/api/tests/test_workflow_api_404.py,sha256=NDE-hu0pSCnn81Sbd6zHDF6ByEPXfO7WuwJnhkAZG6w,6301
|
22
23
|
kailash/channels/__init__.py,sha256=9YpGEpFbNgZ4Fp8fG3F4hEX9X94UR-xbHAQM6b0O1z8,642
|
23
24
|
kailash/channels/api_channel.py,sha256=stuJxNJRUobzj1CavwsHWVZWr2kYqyVLjv_ynt6Wy6A,14541
|
24
25
|
kailash/channels/base.py,sha256=8-cUYAVMlO67BMZCZscShy0FPYAK31s9bxk0JXA9wIM,7825
|
@@ -157,7 +158,7 @@ kailash/monitoring/asyncsql_metrics.py,sha256=jj9M8D5qHoS3zEFfZYsUCWsy5kb-J5-iYV
|
|
157
158
|
kailash/monitoring/metrics.py,sha256=SiAnL3o6K0QaJHgfAuWBa-0pTkW5zymhuPEsj4bgOgM,22022
|
158
159
|
kailash/nodes/__init__.py,sha256=dBnEwrop0cPblHxSOtVWAKCDzhRtcyQVv9j_YGWxczQ,6410
|
159
160
|
kailash/nodes/__init___original.py,sha256=p2KSo0dyUBCLClU123qpQ0tyv5S_36PTxosNyW58nyY,1031
|
160
|
-
kailash/nodes/base.py,sha256=
|
161
|
+
kailash/nodes/base.py,sha256=gDAajw-T7ktbUYfruzfGtoXytT7YF2Cw-3iVkNoox-Y,86550
|
161
162
|
kailash/nodes/base_async.py,sha256=whxepCiVplrltfzEQuabmnGCpEV5WgfqwgxbLdCyiDk,8864
|
162
163
|
kailash/nodes/base_cycle_aware.py,sha256=Xpze9xZzLepWeLpi9Y3tMn1dm2LVv-omr5TSQuGTtWo,13377
|
163
164
|
kailash/nodes/base_with_acl.py,sha256=ZfrkLPgrEBcNbG0LKvtq6glDxyOYOMRw3VXX4vWX6bI,11852
|
@@ -213,7 +214,7 @@ kailash/nodes/cache/cache_invalidation.py,sha256=IUvxrRj3K5EF29Z2EaKl7t6Uze_cssn
|
|
213
214
|
kailash/nodes/cache/redis_pool_manager.py,sha256=GR82GCWxo_gAzRE-091OB6AhKre8CTwM3OoePLb2gvE,21574
|
214
215
|
kailash/nodes/code/__init__.py,sha256=yhEwuMjUEPFfe6hMGMd4E4gZdLUuf2JEQ7knYapiM4o,1283
|
215
216
|
kailash/nodes/code/async_python.py,sha256=Ai-iMpmz-sAori73JBk0wZtqmwtmF2GNPDxqB04I2Ck,37058
|
216
|
-
kailash/nodes/code/python.py,sha256=
|
217
|
+
kailash/nodes/code/python.py,sha256=RbZTXQMNnXXROXKkDYhvKPyYO_6nZ7tJ4EVOeoiABuc,70537
|
217
218
|
kailash/nodes/compliance/__init__.py,sha256=6a_FL4ofc8MAVuZ-ARW5uYenZLS4mBFVM9AI2QsnoF8,214
|
218
219
|
kailash/nodes/compliance/data_retention.py,sha256=90bH_eGwlcDzUdklAJeXQM-RcuLUGQFQ5fgHOK8a4qk,69443
|
219
220
|
kailash/nodes/compliance/gdpr.py,sha256=ZMoHZjAo4QtGwtFCzGMrAUBFV3TbZOnJ5DZGZS87Bas,70548
|
@@ -410,7 +411,7 @@ kailash/workflow/cycle_profiler.py,sha256=aEWSCm0Xy15SjgLTpPooVJMzpFhtJWt4livR-3
|
|
410
411
|
kailash/workflow/cycle_state.py,sha256=hzRUvciRreWfS56Cf7ZLQPit_mlPTQDoNTawh8yi-2s,10747
|
411
412
|
kailash/workflow/cyclic_runner.py,sha256=Zo2Rakp-N5pPSarndYPdkkpWgY9yHpYrrpUh-znoCKQ,71246
|
412
413
|
kailash/workflow/edge_infrastructure.py,sha256=lQDzs0-KdoCMqI4KAXAGbhHbwadM6t-ffJEWLlRuSNo,12448
|
413
|
-
kailash/workflow/graph.py,sha256=
|
414
|
+
kailash/workflow/graph.py,sha256=MhTHRwgsI6KvuLbchsoR10YKGEwCgbQot6t5-RLJdv8,59311
|
414
415
|
kailash/workflow/input_handling.py,sha256=HrW--AmelYC8F18nkfmYlF_wXycA24RuNbDRjvM8rqk,6561
|
415
416
|
kailash/workflow/mermaid_visualizer.py,sha256=CAbLWSL9H-QeRlPNpPOJ3uxIBdcBEtx_KHHrrCdReCU,22290
|
416
417
|
kailash/workflow/migration.py,sha256=6WIGP5Y9kGgb0_3nLXVo8TPtuzsk66eoTSalWt7st_0,31463
|
@@ -423,10 +424,10 @@ kailash/workflow/templates.py,sha256=aZQzEPQD368nN0x0ICQlRKmAr2FqTxIOUa-7rb7EUWI
|
|
423
424
|
kailash/workflow/type_inference.py,sha256=i1F7Yd_Z3elTXrthsLpqGbOnQBIVVVEjhRpI0HrIjd0,24492
|
424
425
|
kailash/workflow/validation.py,sha256=LdbIPQSokCqSLfWTBhJR82pa_0va44pcVu9dpEM4rvY,45177
|
425
426
|
kailash/workflow/visualization.py,sha256=nHBW-Ai8QBMZtn2Nf3EE1_aiMGi9S6Ui_BfpA5KbJPU,23187
|
426
|
-
kailash-0.9.
|
427
|
-
kailash-0.9.
|
428
|
-
kailash-0.9.
|
429
|
-
kailash-0.9.
|
430
|
-
kailash-0.9.
|
431
|
-
kailash-0.9.
|
432
|
-
kailash-0.9.
|
427
|
+
kailash-0.9.22.dist-info/licenses/LICENSE,sha256=9GYZHXVUmx6FdFRNzOeE_w7a_aEGeYbqTVmFtJlrbGk,13438
|
428
|
+
kailash-0.9.22.dist-info/licenses/NOTICE,sha256=9ssIK4LcHSTFqriXGdteMpBPTS1rSLlYtjppZ_bsjZ0,723
|
429
|
+
kailash-0.9.22.dist-info/METADATA,sha256=bsANyjL_bARSOE93lZ0J9ZjeXPsP7kQ28pSl6zFu8hQ,24027
|
430
|
+
kailash-0.9.22.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
431
|
+
kailash-0.9.22.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
|
432
|
+
kailash-0.9.22.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
|
433
|
+
kailash-0.9.22.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|