kailash 0.7.0__py3-none-any.whl → 0.8.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 +1 -1
- kailash/access_control.py +64 -46
- kailash/api/workflow_api.py +34 -3
- kailash/mcp_server/discovery.py +56 -17
- kailash/middleware/communication/api_gateway.py +23 -3
- kailash/middleware/communication/realtime.py +104 -0
- kailash/middleware/core/agent_ui.py +1 -1
- kailash/middleware/gateway/storage_backends.py +393 -0
- kailash/nexus/cli/__init__.py +5 -0
- kailash/nexus/cli/__main__.py +6 -0
- kailash/nexus/cli/main.py +176 -0
- kailash/nodes/__init__.py +6 -5
- kailash/nodes/base.py +29 -5
- kailash/nodes/code/python.py +55 -6
- kailash/nodes/data/async_sql.py +90 -0
- kailash/nodes/security/behavior_analysis.py +414 -0
- kailash/runtime/access_controlled.py +9 -7
- kailash/runtime/runner.py +6 -4
- kailash/runtime/testing.py +1 -1
- kailash/security.py +6 -2
- kailash/servers/enterprise_workflow_server.py +58 -2
- kailash/servers/workflow_server.py +3 -0
- kailash/workflow/builder.py +102 -14
- kailash/workflow/cyclic_runner.py +102 -10
- kailash/workflow/visualization.py +117 -27
- {kailash-0.7.0.dist-info → kailash-0.8.1.dist-info}/METADATA +4 -2
- {kailash-0.7.0.dist-info → kailash-0.8.1.dist-info}/RECORD +31 -28
- kailash/workflow/builder_improvements.py +0 -207
- {kailash-0.7.0.dist-info → kailash-0.8.1.dist-info}/WHEEL +0 -0
- {kailash-0.7.0.dist-info → kailash-0.8.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.7.0.dist-info → kailash-0.8.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.7.0.dist-info → kailash-0.8.1.dist-info}/top_level.txt +0 -0
kailash/nodes/code/python.py
CHANGED
@@ -50,6 +50,7 @@ import ast
|
|
50
50
|
import importlib.util
|
51
51
|
import inspect
|
52
52
|
import logging
|
53
|
+
import os
|
53
54
|
import resource
|
54
55
|
import traceback
|
55
56
|
from collections.abc import Callable
|
@@ -94,6 +95,7 @@ ALLOWED_MODULES = {
|
|
94
95
|
"matplotlib",
|
95
96
|
"seaborn",
|
96
97
|
"plotly",
|
98
|
+
"array", # Required by numpy internally
|
97
99
|
# File processing modules
|
98
100
|
"csv", # For CSV file processing
|
99
101
|
"mimetypes", # For MIME type detection
|
@@ -419,12 +421,59 @@ class CodeExecutor:
|
|
419
421
|
}
|
420
422
|
|
421
423
|
# Add allowed modules
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
424
|
+
# Check if we're running under coverage to avoid instrumentation conflicts
|
425
|
+
import sys
|
426
|
+
|
427
|
+
if "coverage" in sys.modules:
|
428
|
+
# Under coverage, use lazy loading for problematic modules
|
429
|
+
problematic_modules = {
|
430
|
+
"numpy",
|
431
|
+
"scipy",
|
432
|
+
"sklearn",
|
433
|
+
"pandas",
|
434
|
+
"matplotlib",
|
435
|
+
"seaborn",
|
436
|
+
"plotly",
|
437
|
+
"array",
|
438
|
+
}
|
439
|
+
safe_modules = self.allowed_modules - problematic_modules
|
440
|
+
|
441
|
+
# Eagerly load safe modules
|
442
|
+
for module_name in safe_modules:
|
443
|
+
try:
|
444
|
+
module = importlib.import_module(module_name)
|
445
|
+
namespace[module_name] = module
|
446
|
+
except ImportError:
|
447
|
+
logger.warning(f"Module {module_name} not available")
|
448
|
+
|
449
|
+
# Add lazy loader for problematic modules
|
450
|
+
class LazyModuleLoader:
|
451
|
+
def __getattr__(self, name):
|
452
|
+
if name in problematic_modules:
|
453
|
+
return importlib.import_module(name)
|
454
|
+
raise AttributeError(f"Module {name} not found")
|
455
|
+
|
456
|
+
# Make problematic modules available through lazy loading
|
457
|
+
for module_name in problematic_modules:
|
458
|
+
try:
|
459
|
+
# Try to import the module directly
|
460
|
+
module = importlib.import_module(module_name)
|
461
|
+
namespace[module_name] = module
|
462
|
+
except ImportError:
|
463
|
+
# If import fails, use lazy loader as fallback
|
464
|
+
namespace[module_name] = LazyModuleLoader()
|
465
|
+
else:
|
466
|
+
# Normal operation - eagerly load all modules
|
467
|
+
for module_name in self.allowed_modules:
|
468
|
+
try:
|
469
|
+
# Skip scipy in CI due to version conflicts
|
470
|
+
if module_name == "scipy" and os.environ.get("CI"):
|
471
|
+
logger.warning("Skipping scipy import in CI environment")
|
472
|
+
continue
|
473
|
+
module = importlib.import_module(module_name)
|
474
|
+
namespace[module_name] = module
|
475
|
+
except ImportError:
|
476
|
+
logger.warning(f"Module {module_name} not available")
|
428
477
|
|
429
478
|
# Add sanitized inputs
|
430
479
|
namespace.update(sanitized_inputs)
|
kailash/nodes/data/async_sql.py
CHANGED
@@ -431,6 +431,7 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
431
431
|
fetch_mode: FetchMode = FetchMode.ALL,
|
432
432
|
fetch_size: Optional[int] = None,
|
433
433
|
transaction: Optional[Any] = None,
|
434
|
+
parameter_types: Optional[dict[str, str]] = None,
|
434
435
|
) -> Any:
|
435
436
|
"""Execute query and return results."""
|
436
437
|
# Convert dict params to positional for asyncpg
|
@@ -440,8 +441,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
440
441
|
import json
|
441
442
|
|
442
443
|
query_params = []
|
444
|
+
param_names = [] # Track parameter names for type mapping
|
443
445
|
for i, (key, value) in enumerate(params.items(), 1):
|
444
446
|
query = query.replace(f":{key}", f"${i}")
|
447
|
+
param_names.append(key)
|
445
448
|
# For PostgreSQL, lists should remain as lists for array operations
|
446
449
|
# Only convert dicts to JSON strings
|
447
450
|
if isinstance(value, dict):
|
@@ -449,6 +452,24 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
449
452
|
query_params.append(value)
|
450
453
|
params = query_params
|
451
454
|
|
455
|
+
# Apply parameter type casts if provided
|
456
|
+
if parameter_types:
|
457
|
+
# Build a query with explicit type casts
|
458
|
+
for i, param_name in enumerate(param_names, 1):
|
459
|
+
if param_name in parameter_types:
|
460
|
+
pg_type = parameter_types[param_name]
|
461
|
+
# Replace $N with $N::type in the query
|
462
|
+
query = query.replace(f"${i}", f"${i}::{pg_type}")
|
463
|
+
|
464
|
+
else:
|
465
|
+
# For positional params, apply type casts if provided
|
466
|
+
if parameter_types and isinstance(params, (list, tuple)):
|
467
|
+
# Build query with type casts for positional parameters
|
468
|
+
for i, param_type in parameter_types.items():
|
469
|
+
if isinstance(i, int) and 0 <= i < len(params):
|
470
|
+
# Replace $N with $N::type
|
471
|
+
query = query.replace(f"${i+1}", f"${i+1}::{param_type}")
|
472
|
+
|
452
473
|
# Ensure params is a list/tuple for asyncpg
|
453
474
|
if params is None:
|
454
475
|
params = []
|
@@ -1270,6 +1291,13 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
1270
1291
|
default=False,
|
1271
1292
|
description="Whether to allow administrative SQL commands (CREATE, DROP, etc.)",
|
1272
1293
|
),
|
1294
|
+
NodeParameter(
|
1295
|
+
name="parameter_types",
|
1296
|
+
type=dict,
|
1297
|
+
required=False,
|
1298
|
+
default=None,
|
1299
|
+
description="Optional PostgreSQL type hints for parameters (e.g., {'role_id': 'text', 'metadata': 'jsonb'})",
|
1300
|
+
),
|
1273
1301
|
NodeParameter(
|
1274
1302
|
name="retry_config",
|
1275
1303
|
type=Any,
|
@@ -1532,6 +1560,9 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
1532
1560
|
"result_format", self.config.get("result_format", "dict")
|
1533
1561
|
)
|
1534
1562
|
user_context = inputs.get("user_context")
|
1563
|
+
parameter_types = inputs.get(
|
1564
|
+
"parameter_types", self.config.get("parameter_types")
|
1565
|
+
)
|
1535
1566
|
|
1536
1567
|
if not query:
|
1537
1568
|
raise NodeExecutionError("No query provided")
|
@@ -1576,8 +1607,12 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
1576
1607
|
fetch_mode=fetch_mode,
|
1577
1608
|
fetch_size=fetch_size,
|
1578
1609
|
user_context=user_context,
|
1610
|
+
parameter_types=parameter_types,
|
1579
1611
|
)
|
1580
1612
|
|
1613
|
+
# Ensure all data is JSON-serializable (safety net for adapter inconsistencies)
|
1614
|
+
result = self._ensure_serializable(result)
|
1615
|
+
|
1581
1616
|
# Format results based on requested format
|
1582
1617
|
formatted_data = self._format_results(result, result_format)
|
1583
1618
|
|
@@ -1795,6 +1830,7 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
1795
1830
|
fetch_mode: FetchMode,
|
1796
1831
|
fetch_size: Optional[int],
|
1797
1832
|
user_context: Any = None,
|
1833
|
+
parameter_types: Optional[dict[str, str]] = None,
|
1798
1834
|
) -> Any:
|
1799
1835
|
"""Execute query with retry logic for transient failures.
|
1800
1836
|
|
@@ -1823,6 +1859,7 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
1823
1859
|
params=params,
|
1824
1860
|
fetch_mode=fetch_mode,
|
1825
1861
|
fetch_size=fetch_size,
|
1862
|
+
parameter_types=parameter_types,
|
1826
1863
|
)
|
1827
1864
|
|
1828
1865
|
# Apply data masking if access control is enabled
|
@@ -2010,6 +2047,7 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
2010
2047
|
params: Any,
|
2011
2048
|
fetch_mode: FetchMode,
|
2012
2049
|
fetch_size: Optional[int],
|
2050
|
+
parameter_types: Optional[dict[str, str]] = None,
|
2013
2051
|
) -> Any:
|
2014
2052
|
"""Execute query with automatic transaction management.
|
2015
2053
|
|
@@ -2034,6 +2072,7 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
2034
2072
|
fetch_mode=fetch_mode,
|
2035
2073
|
fetch_size=fetch_size,
|
2036
2074
|
transaction=self._active_transaction,
|
2075
|
+
parameter_types=parameter_types,
|
2037
2076
|
)
|
2038
2077
|
elif self._transaction_mode == "auto":
|
2039
2078
|
# Auto-transaction mode
|
@@ -2045,6 +2084,7 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
2045
2084
|
fetch_mode=fetch_mode,
|
2046
2085
|
fetch_size=fetch_size,
|
2047
2086
|
transaction=transaction,
|
2087
|
+
parameter_types=parameter_types,
|
2048
2088
|
)
|
2049
2089
|
await adapter.commit_transaction(transaction)
|
2050
2090
|
return result
|
@@ -2058,6 +2098,7 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
2058
2098
|
params=params,
|
2059
2099
|
fetch_mode=fetch_mode,
|
2060
2100
|
fetch_size=fetch_size,
|
2101
|
+
parameter_types=parameter_types,
|
2061
2102
|
)
|
2062
2103
|
|
2063
2104
|
@classmethod
|
@@ -2460,6 +2501,55 @@ class AsyncSQLDatabaseNode(AsyncNode):
|
|
2460
2501
|
|
2461
2502
|
return modified_query, param_dict
|
2462
2503
|
|
2504
|
+
def _ensure_serializable(self, data: Any) -> Any:
|
2505
|
+
"""Ensure all data types are JSON-serializable.
|
2506
|
+
|
2507
|
+
This is a safety net for cases where adapter _convert_row might not be called
|
2508
|
+
or might miss certain data types. It recursively processes the data structure
|
2509
|
+
to ensure datetime objects and other non-JSON-serializable types are converted.
|
2510
|
+
|
2511
|
+
Args:
|
2512
|
+
data: Raw data from database adapter
|
2513
|
+
|
2514
|
+
Returns:
|
2515
|
+
JSON-serializable data structure
|
2516
|
+
"""
|
2517
|
+
if data is None:
|
2518
|
+
return None
|
2519
|
+
elif isinstance(data, bool):
|
2520
|
+
return data
|
2521
|
+
elif isinstance(data, (int, float, str)):
|
2522
|
+
return data
|
2523
|
+
elif isinstance(data, datetime):
|
2524
|
+
return data.isoformat()
|
2525
|
+
elif isinstance(data, date):
|
2526
|
+
return data.isoformat()
|
2527
|
+
elif hasattr(data, "total_seconds"): # timedelta
|
2528
|
+
return data.total_seconds()
|
2529
|
+
elif isinstance(data, Decimal):
|
2530
|
+
return float(data)
|
2531
|
+
elif isinstance(data, bytes):
|
2532
|
+
import base64
|
2533
|
+
|
2534
|
+
return base64.b64encode(data).decode("utf-8")
|
2535
|
+
elif hasattr(data, "__str__") and hasattr(data, "hex"): # UUID-like objects
|
2536
|
+
return str(data)
|
2537
|
+
elif isinstance(data, dict):
|
2538
|
+
return {
|
2539
|
+
key: self._ensure_serializable(value) for key, value in data.items()
|
2540
|
+
}
|
2541
|
+
elif isinstance(data, (list, tuple)):
|
2542
|
+
return [self._ensure_serializable(item) for item in data]
|
2543
|
+
else:
|
2544
|
+
# For any other type, try to convert to string as fallback
|
2545
|
+
try:
|
2546
|
+
# Test if it's already JSON serializable
|
2547
|
+
json.dumps(data)
|
2548
|
+
return data
|
2549
|
+
except (TypeError, ValueError):
|
2550
|
+
# Not serializable, convert to string
|
2551
|
+
return str(data)
|
2552
|
+
|
2463
2553
|
def _format_results(self, data: list[dict], result_format: str) -> Any:
|
2464
2554
|
"""Format query results according to specified format.
|
2465
2555
|
|