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.
@@ -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
- for module_name in self.allowed_modules:
423
- try:
424
- module = importlib.import_module(module_name)
425
- namespace[module_name] = module
426
- except ImportError:
427
- logger.warning(f"Module {module_name} not available")
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)
@@ -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