kailash 0.9.0__py3-none-any.whl → 0.9.2__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 +6 -4
- kailash/mcp_server/client.py +371 -6
- kailash/nodes/data/async_sql.py +32 -8
- {kailash-0.9.0.dist-info → kailash-0.9.2.dist-info}/METADATA +1 -1
- {kailash-0.9.0.dist-info → kailash-0.9.2.dist-info}/RECORD +9 -9
- {kailash-0.9.0.dist-info → kailash-0.9.2.dist-info}/WHEEL +0 -0
- {kailash-0.9.0.dist-info → kailash-0.9.2.dist-info}/entry_points.txt +0 -0
- {kailash-0.9.0.dist-info → kailash-0.9.2.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.9.0.dist-info → kailash-0.9.2.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
@@ -3,9 +3,11 @@
|
|
3
3
|
The Kailash SDK provides a comprehensive framework for creating nodes and workflows
|
4
4
|
that align with container-node architecture while allowing rapid prototyping.
|
5
5
|
|
6
|
-
New in v0.9.
|
7
|
-
|
8
|
-
Previous v0.
|
6
|
+
New in v0.9.2: WebSocket Transport Support with Enterprise Connection Pooling.
|
7
|
+
Fixed "Unsupported transport: websocket" error. Added 73% performance improvement with connection pooling.
|
8
|
+
Previous v0.9.1: DataFlow PostgreSQL parameter conversion verification and comprehensive testing infrastructure.
|
9
|
+
Complete validation of parameter conversion chain from DataFlow to AsyncSQLDatabaseNode to PostgreSQL.
|
10
|
+
Previous v0.9.0: Complete migration from cycle=True to modern CycleBuilder API.
|
9
11
|
"""
|
10
12
|
|
11
13
|
from kailash.nodes.base import Node, NodeMetadata, NodeParameter
|
@@ -48,7 +50,7 @@ except ImportError:
|
|
48
50
|
# For backward compatibility
|
49
51
|
WorkflowGraph = Workflow
|
50
52
|
|
51
|
-
__version__ = "0.9.
|
53
|
+
__version__ = "0.9.2"
|
52
54
|
|
53
55
|
__all__ = [
|
54
56
|
# Core workflow components
|
kailash/mcp_server/client.py
CHANGED
@@ -7,7 +7,7 @@ import os
|
|
7
7
|
import time
|
8
8
|
import uuid
|
9
9
|
from contextlib import AsyncExitStack
|
10
|
-
from typing import Any, Dict, List, Optional, Union
|
10
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
11
11
|
|
12
12
|
from .auth import AuthManager, AuthProvider, PermissionManager, RateLimiter
|
13
13
|
from .errors import (
|
@@ -98,7 +98,9 @@ class MCPClient:
|
|
98
98
|
|
99
99
|
# Connection pooling
|
100
100
|
self.connection_pool_config = connection_pool_config or {}
|
101
|
-
self.
|
101
|
+
self._websocket_pools: Dict[str, Any] = {} # url -> connection info
|
102
|
+
self._pool_lock = asyncio.Lock()
|
103
|
+
self._connection_last_used: Dict[str, float] = {}
|
102
104
|
|
103
105
|
# Metrics
|
104
106
|
if enable_metrics:
|
@@ -137,6 +139,8 @@ class MCPClient:
|
|
137
139
|
|
138
140
|
# Update transport usage metrics
|
139
141
|
if self.metrics:
|
142
|
+
if "transport_usage" not in self.metrics:
|
143
|
+
self.metrics["transport_usage"] = {}
|
140
144
|
transport_counts = self.metrics["transport_usage"]
|
141
145
|
transport_counts[transport_type] = (
|
142
146
|
transport_counts.get(transport_type, 0) + 1
|
@@ -148,6 +152,8 @@ class MCPClient:
|
|
148
152
|
return await self._discover_tools_sse(server_config, timeout)
|
149
153
|
elif transport_type == "http":
|
150
154
|
return await self._discover_tools_http(server_config, timeout)
|
155
|
+
elif transport_type == "websocket":
|
156
|
+
return await self._discover_tools_websocket(server_config, timeout)
|
151
157
|
else:
|
152
158
|
raise TransportError(
|
153
159
|
f"Unsupported transport: {transport_type}",
|
@@ -173,7 +179,8 @@ class MCPClient:
|
|
173
179
|
|
174
180
|
except Exception as e:
|
175
181
|
if self.metrics:
|
176
|
-
|
182
|
+
if "requests_failed" in self.metrics:
|
183
|
+
self.metrics["requests_failed"] += 1
|
177
184
|
|
178
185
|
logger.error(f"Failed to discover tools from {server_key}: {e}")
|
179
186
|
return []
|
@@ -309,6 +316,67 @@ class MCPClient:
|
|
309
316
|
|
310
317
|
return tools
|
311
318
|
|
319
|
+
async def _discover_tools_websocket(
|
320
|
+
self, server_config: Union[str, Dict[str, Any]], timeout: Optional[float]
|
321
|
+
) -> List[Dict[str, Any]]:
|
322
|
+
"""Discover tools using WebSocket transport."""
|
323
|
+
from mcp import ClientSession
|
324
|
+
from mcp.client.websocket import websocket_client
|
325
|
+
|
326
|
+
# Extract WebSocket URL from server config
|
327
|
+
if isinstance(server_config, str):
|
328
|
+
url = server_config
|
329
|
+
else:
|
330
|
+
url = server_config.get("url")
|
331
|
+
if not url:
|
332
|
+
raise TransportError(
|
333
|
+
"WebSocket URL not provided", transport_type="websocket"
|
334
|
+
)
|
335
|
+
|
336
|
+
# Get or create connection from pool
|
337
|
+
session, is_new = await self._get_or_create_websocket_connection(url, timeout)
|
338
|
+
|
339
|
+
# Update metrics
|
340
|
+
if self.metrics:
|
341
|
+
if is_new:
|
342
|
+
self.metrics["websocket_pool_misses"] = (
|
343
|
+
self.metrics.get("websocket_pool_misses", 0) + 1
|
344
|
+
)
|
345
|
+
self.metrics["websocket_connections_created"] = (
|
346
|
+
self.metrics.get("websocket_connections_created", 0) + 1
|
347
|
+
)
|
348
|
+
else:
|
349
|
+
self.metrics["websocket_pool_hits"] = (
|
350
|
+
self.metrics.get("websocket_pool_hits", 0) + 1
|
351
|
+
)
|
352
|
+
self.metrics["websocket_connections_reused"] = (
|
353
|
+
self.metrics.get("websocket_connections_reused", 0) + 1
|
354
|
+
)
|
355
|
+
|
356
|
+
try:
|
357
|
+
# List tools with timeout
|
358
|
+
if timeout:
|
359
|
+
result = await asyncio.wait_for(session.list_tools(), timeout=timeout)
|
360
|
+
else:
|
361
|
+
result = await session.list_tools()
|
362
|
+
|
363
|
+
# Convert to standard format
|
364
|
+
tools = []
|
365
|
+
for tool in result.tools:
|
366
|
+
tools.append(
|
367
|
+
{
|
368
|
+
"name": tool.name,
|
369
|
+
"description": tool.description,
|
370
|
+
"parameters": tool.inputSchema,
|
371
|
+
}
|
372
|
+
)
|
373
|
+
|
374
|
+
return tools
|
375
|
+
except Exception as e:
|
376
|
+
# On error, remove connection from pool
|
377
|
+
await self._remove_connection_from_pool(url)
|
378
|
+
raise
|
379
|
+
|
312
380
|
async def call_tool(
|
313
381
|
self,
|
314
382
|
server_config: Union[str, Dict[str, Any]],
|
@@ -350,6 +418,10 @@ class MCPClient:
|
|
350
418
|
return await self._call_tool_http(
|
351
419
|
server_config, tool_name, arguments, timeout
|
352
420
|
)
|
421
|
+
elif transport_type == "websocket":
|
422
|
+
return await self._call_tool_websocket(
|
423
|
+
server_config, tool_name, arguments, timeout
|
424
|
+
)
|
353
425
|
else:
|
354
426
|
raise TransportError(
|
355
427
|
f"Unsupported transport: {transport_type}",
|
@@ -372,7 +444,8 @@ class MCPClient:
|
|
372
444
|
|
373
445
|
except Exception as e:
|
374
446
|
if self.metrics:
|
375
|
-
|
447
|
+
if "requests_failed" in self.metrics:
|
448
|
+
self.metrics["requests_failed"] += 1
|
376
449
|
|
377
450
|
logger.error(f"Tool call failed for {tool_name}: {e}")
|
378
451
|
return {"success": False, "error": str(e), "tool_name": tool_name}
|
@@ -524,6 +597,77 @@ class MCPClient:
|
|
524
597
|
"tool_name": tool_name,
|
525
598
|
}
|
526
599
|
|
600
|
+
async def _call_tool_websocket(
|
601
|
+
self,
|
602
|
+
server_config: Union[str, Dict[str, Any]],
|
603
|
+
tool_name: str,
|
604
|
+
arguments: Dict[str, Any],
|
605
|
+
timeout: Optional[float],
|
606
|
+
) -> Dict[str, Any]:
|
607
|
+
"""Call tool using WebSocket transport."""
|
608
|
+
from mcp import ClientSession
|
609
|
+
from mcp.client.websocket import websocket_client
|
610
|
+
|
611
|
+
# Extract WebSocket URL from server config
|
612
|
+
if isinstance(server_config, str):
|
613
|
+
url = server_config
|
614
|
+
else:
|
615
|
+
url = server_config.get("url")
|
616
|
+
if not url:
|
617
|
+
raise TransportError(
|
618
|
+
"WebSocket URL not provided", transport_type="websocket"
|
619
|
+
)
|
620
|
+
|
621
|
+
# Get or create connection from pool
|
622
|
+
session, is_new = await self._get_or_create_websocket_connection(url, timeout)
|
623
|
+
|
624
|
+
# Update metrics
|
625
|
+
if self.metrics:
|
626
|
+
if is_new:
|
627
|
+
self.metrics["websocket_pool_misses"] = (
|
628
|
+
self.metrics.get("websocket_pool_misses", 0) + 1
|
629
|
+
)
|
630
|
+
self.metrics["websocket_connections_created"] = (
|
631
|
+
self.metrics.get("websocket_connections_created", 0) + 1
|
632
|
+
)
|
633
|
+
else:
|
634
|
+
self.metrics["websocket_pool_hits"] = (
|
635
|
+
self.metrics.get("websocket_pool_hits", 0) + 1
|
636
|
+
)
|
637
|
+
self.metrics["websocket_connections_reused"] = (
|
638
|
+
self.metrics.get("websocket_connections_reused", 0) + 1
|
639
|
+
)
|
640
|
+
|
641
|
+
try:
|
642
|
+
# Call tool with timeout
|
643
|
+
if timeout:
|
644
|
+
result = await asyncio.wait_for(
|
645
|
+
session.call_tool(name=tool_name, arguments=arguments),
|
646
|
+
timeout=timeout,
|
647
|
+
)
|
648
|
+
else:
|
649
|
+
result = await session.call_tool(name=tool_name, arguments=arguments)
|
650
|
+
|
651
|
+
# Extract content from result
|
652
|
+
content = []
|
653
|
+
if hasattr(result, "content"):
|
654
|
+
for item in result.content:
|
655
|
+
if hasattr(item, "text"):
|
656
|
+
content.append(item.text)
|
657
|
+
else:
|
658
|
+
content.append(str(item))
|
659
|
+
|
660
|
+
return {
|
661
|
+
"success": True,
|
662
|
+
"content": "\n".join(content) if content else "",
|
663
|
+
"result": result,
|
664
|
+
"tool_name": tool_name,
|
665
|
+
}
|
666
|
+
except Exception as e:
|
667
|
+
# On error, remove connection from pool
|
668
|
+
await self._remove_connection_from_pool(url)
|
669
|
+
raise
|
670
|
+
|
527
671
|
# Additional enhanced methods
|
528
672
|
async def list_resources(
|
529
673
|
self,
|
@@ -582,7 +726,12 @@ class MCPClient:
|
|
582
726
|
def _get_transport_type(self, server_config: Union[str, Dict[str, Any]]) -> str:
|
583
727
|
"""Determine transport type from server config."""
|
584
728
|
if isinstance(server_config, str):
|
585
|
-
|
729
|
+
if server_config.startswith(("ws://", "wss://")):
|
730
|
+
return "websocket"
|
731
|
+
elif server_config.startswith(("http://", "https://")):
|
732
|
+
return "sse"
|
733
|
+
else:
|
734
|
+
return "stdio"
|
586
735
|
else:
|
587
736
|
return server_config.get("transport", "stdio")
|
588
737
|
|
@@ -596,7 +745,7 @@ class MCPClient:
|
|
596
745
|
command = server_config.get("command", "python")
|
597
746
|
args = server_config.get("args", [])
|
598
747
|
return f"stdio://{command}:{':'.join(args)}"
|
599
|
-
elif transport in ["sse", "http"]:
|
748
|
+
elif transport in ["sse", "http", "websocket"]:
|
600
749
|
return server_config.get("url", "unknown")
|
601
750
|
else:
|
602
751
|
return str(hash(json.dumps(server_config, sort_keys=True)))
|
@@ -710,3 +859,219 @@ class MCPClient:
|
|
710
859
|
"timestamp": str(time.time()),
|
711
860
|
},
|
712
861
|
}
|
862
|
+
|
863
|
+
# WebSocket Connection Pooling Methods
|
864
|
+
async def _get_or_create_websocket_connection(
|
865
|
+
self, url: str, timeout: Optional[float] = None
|
866
|
+
) -> Tuple[Any, bool]:
|
867
|
+
"""Get existing connection from pool or create a new one.
|
868
|
+
|
869
|
+
Returns:
|
870
|
+
Tuple of (session, is_new_connection)
|
871
|
+
"""
|
872
|
+
# Check if pooling is enabled
|
873
|
+
if not self._should_use_pooling():
|
874
|
+
# Create new connection without pooling
|
875
|
+
session = await self._create_websocket_connection(url, timeout)
|
876
|
+
return session, True
|
877
|
+
|
878
|
+
async with self._pool_lock:
|
879
|
+
# Update last used time
|
880
|
+
self._connection_last_used[url] = time.time()
|
881
|
+
|
882
|
+
# Check if we have an existing healthy connection
|
883
|
+
if url in self._websocket_pools:
|
884
|
+
conn_info = self._websocket_pools[url]
|
885
|
+
session = conn_info.get("session")
|
886
|
+
|
887
|
+
# Check if connection is still healthy
|
888
|
+
if session and await self._is_connection_healthy(session):
|
889
|
+
return session, False
|
890
|
+
else:
|
891
|
+
# Remove unhealthy connection
|
892
|
+
del self._websocket_pools[url]
|
893
|
+
|
894
|
+
# Check pool size limits
|
895
|
+
if len(self._websocket_pools) >= self.connection_pool_config.get(
|
896
|
+
"max_connections", 10
|
897
|
+
):
|
898
|
+
# Evict least recently used connection
|
899
|
+
await self._evict_lru_connection()
|
900
|
+
|
901
|
+
# Create new connection
|
902
|
+
session = await self._create_websocket_connection(url, timeout)
|
903
|
+
|
904
|
+
# Store in pool
|
905
|
+
self._websocket_pools[url] = {
|
906
|
+
"session": session,
|
907
|
+
"created_at": time.time(),
|
908
|
+
"url": url,
|
909
|
+
}
|
910
|
+
|
911
|
+
return session, True
|
912
|
+
|
913
|
+
async def _create_websocket_connection(
|
914
|
+
self, url: str, timeout: Optional[float]
|
915
|
+
) -> Any:
|
916
|
+
"""Create a new WebSocket connection and session."""
|
917
|
+
# Create connection using AsyncExitStack for proper lifecycle management
|
918
|
+
# This fixes the manual __aenter__/__aexit__ issue
|
919
|
+
from contextlib import AsyncExitStack
|
920
|
+
|
921
|
+
from mcp import ClientSession
|
922
|
+
from mcp.client.websocket import websocket_client
|
923
|
+
|
924
|
+
class WebSocketConnection:
|
925
|
+
def __init__(self):
|
926
|
+
self.exit_stack = None
|
927
|
+
self.session = None
|
928
|
+
|
929
|
+
async def connect(self, url):
|
930
|
+
# Use AsyncExitStack to properly manage async context managers
|
931
|
+
self.exit_stack = AsyncExitStack()
|
932
|
+
|
933
|
+
try:
|
934
|
+
# Enter the websocket context using AsyncExitStack
|
935
|
+
websocket_context = websocket_client(url=url)
|
936
|
+
streams = await self.exit_stack.enter_async_context(
|
937
|
+
websocket_context
|
938
|
+
)
|
939
|
+
self.read_stream, self.write_stream = streams
|
940
|
+
|
941
|
+
# Create and initialize session using AsyncExitStack
|
942
|
+
session = ClientSession(self.read_stream, self.write_stream)
|
943
|
+
session_ref = await self.exit_stack.enter_async_context(session)
|
944
|
+
await session_ref.initialize()
|
945
|
+
|
946
|
+
self.session = session_ref
|
947
|
+
return session_ref
|
948
|
+
|
949
|
+
except Exception:
|
950
|
+
# If anything fails during setup, clean up
|
951
|
+
await self.close()
|
952
|
+
raise
|
953
|
+
|
954
|
+
async def close(self):
|
955
|
+
# Handle cleanup with proper exception isolation
|
956
|
+
if self.exit_stack:
|
957
|
+
exit_stack = self.exit_stack
|
958
|
+
self.exit_stack = None
|
959
|
+
self.session = None
|
960
|
+
|
961
|
+
# Schedule cleanup for later to avoid cross-task issues
|
962
|
+
# This prevents the "different task" async generator problems
|
963
|
+
try:
|
964
|
+
# Use create_task to run cleanup independently
|
965
|
+
cleanup_task = asyncio.create_task(exit_stack.aclose())
|
966
|
+
|
967
|
+
# Don't await - let it run in background to avoid blocking
|
968
|
+
# But add a callback to log any errors
|
969
|
+
def log_cleanup_error(task):
|
970
|
+
if task.exception():
|
971
|
+
logger.warning(
|
972
|
+
f"Background cleanup error: {task.exception()}"
|
973
|
+
)
|
974
|
+
|
975
|
+
cleanup_task.add_done_callback(log_cleanup_error)
|
976
|
+
except Exception as e:
|
977
|
+
logger.warning(f"Error scheduling connection cleanup: {e}")
|
978
|
+
|
979
|
+
# Create and connect
|
980
|
+
conn = WebSocketConnection()
|
981
|
+
session = await conn.connect(url)
|
982
|
+
|
983
|
+
# Store connection object for cleanup
|
984
|
+
if not hasattr(self, "_websocket_connections"):
|
985
|
+
self._websocket_connections = {}
|
986
|
+
self._websocket_connections[url] = conn
|
987
|
+
|
988
|
+
return session
|
989
|
+
|
990
|
+
async def _remove_connection_from_pool(self, url: str):
|
991
|
+
"""Remove a connection from the pool."""
|
992
|
+
async with self._pool_lock:
|
993
|
+
if url in self._websocket_pools:
|
994
|
+
del self._websocket_pools[url]
|
995
|
+
|
996
|
+
# Clean up connection
|
997
|
+
if (
|
998
|
+
hasattr(self, "_websocket_connections")
|
999
|
+
and url in self._websocket_connections
|
1000
|
+
):
|
1001
|
+
conn = self._websocket_connections[url]
|
1002
|
+
try:
|
1003
|
+
await conn.close()
|
1004
|
+
except Exception as e:
|
1005
|
+
logger.warning(f"Error closing WebSocket connection: {e}")
|
1006
|
+
finally:
|
1007
|
+
del self._websocket_connections[url]
|
1008
|
+
|
1009
|
+
# Clean up last used tracking
|
1010
|
+
if url in self._connection_last_used:
|
1011
|
+
del self._connection_last_used[url]
|
1012
|
+
|
1013
|
+
def _should_use_pooling(self) -> bool:
|
1014
|
+
"""Check if connection pooling should be used."""
|
1015
|
+
return self.connection_pool_config.get("enable_connection_reuse", True)
|
1016
|
+
|
1017
|
+
def _get_active_connections(self) -> List[str]:
|
1018
|
+
"""Get list of active connection URLs."""
|
1019
|
+
return list(self._websocket_pools.keys())
|
1020
|
+
|
1021
|
+
def _has_active_connection(self, url: str) -> bool:
|
1022
|
+
"""Check if URL has an active connection."""
|
1023
|
+
return url in self._websocket_pools
|
1024
|
+
|
1025
|
+
async def _is_connection_healthy(self, session: Any) -> bool:
|
1026
|
+
"""Check if a connection is healthy."""
|
1027
|
+
try:
|
1028
|
+
# Try to ping if method exists
|
1029
|
+
if hasattr(session, "ping"):
|
1030
|
+
await asyncio.wait_for(session.ping(), timeout=5.0)
|
1031
|
+
return True
|
1032
|
+
except Exception:
|
1033
|
+
return False
|
1034
|
+
|
1035
|
+
async def _check_connection_health(self, url: str):
|
1036
|
+
"""Check and update health status of a connection."""
|
1037
|
+
if url not in self._websocket_pools:
|
1038
|
+
return
|
1039
|
+
|
1040
|
+
session = self._websocket_pools[url].get("session")
|
1041
|
+
if session and not await self._is_connection_healthy(session):
|
1042
|
+
# Remove unhealthy connection
|
1043
|
+
await self._remove_connection_from_pool(url)
|
1044
|
+
|
1045
|
+
async def _cleanup_idle_connections(self, max_idle_seconds: float = None):
|
1046
|
+
"""Clean up idle connections."""
|
1047
|
+
if max_idle_seconds is None:
|
1048
|
+
max_idle_seconds = self.connection_pool_config.get("max_idle_time", 60)
|
1049
|
+
|
1050
|
+
current_time = time.time()
|
1051
|
+
urls_to_remove = []
|
1052
|
+
|
1053
|
+
async with self._pool_lock:
|
1054
|
+
for url, last_used in self._connection_last_used.items():
|
1055
|
+
if current_time - last_used > max_idle_seconds:
|
1056
|
+
urls_to_remove.append(url)
|
1057
|
+
|
1058
|
+
# Remove idle connections
|
1059
|
+
for url in urls_to_remove:
|
1060
|
+
await self._remove_connection_from_pool(url)
|
1061
|
+
|
1062
|
+
async def _evict_lru_connection(self):
|
1063
|
+
"""Evict least recently used connection."""
|
1064
|
+
if not self._connection_last_used:
|
1065
|
+
return
|
1066
|
+
|
1067
|
+
# Find LRU connection
|
1068
|
+
lru_url = min(self._connection_last_used, key=self._connection_last_used.get)
|
1069
|
+
|
1070
|
+
# Update metrics
|
1071
|
+
if self.metrics:
|
1072
|
+
self.metrics["websocket_pool_evictions"] = (
|
1073
|
+
self.metrics.get("websocket_pool_evictions", 0) + 1
|
1074
|
+
)
|
1075
|
+
|
1076
|
+
# Remove it
|
1077
|
+
await self._remove_connection_from_pool(lru_url)
|
kailash/nodes/data/async_sql.py
CHANGED
@@ -493,7 +493,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
493
493
|
and "RETURNING" not in query_upper
|
494
494
|
and fetch_mode == FetchMode.ALL
|
495
495
|
):
|
496
|
-
|
496
|
+
if isinstance(params, dict):
|
497
|
+
result = await conn.execute(query, params)
|
498
|
+
else:
|
499
|
+
result = await conn.execute(query, *params)
|
497
500
|
# asyncpg returns a string like "UPDATE 1", extract the count
|
498
501
|
if isinstance(result, str):
|
499
502
|
parts = result.split()
|
@@ -505,15 +508,24 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
505
508
|
return []
|
506
509
|
|
507
510
|
if fetch_mode == FetchMode.ONE:
|
508
|
-
|
511
|
+
if isinstance(params, dict):
|
512
|
+
row = await conn.fetchrow(query, params)
|
513
|
+
else:
|
514
|
+
row = await conn.fetchrow(query, *params)
|
509
515
|
return self._convert_row(dict(row)) if row else None
|
510
516
|
elif fetch_mode == FetchMode.ALL:
|
511
|
-
|
517
|
+
if isinstance(params, dict):
|
518
|
+
rows = await conn.fetch(query, params)
|
519
|
+
else:
|
520
|
+
rows = await conn.fetch(query, *params)
|
512
521
|
return [self._convert_row(dict(row)) for row in rows]
|
513
522
|
elif fetch_mode == FetchMode.MANY:
|
514
523
|
if not fetch_size:
|
515
524
|
raise ValueError("fetch_size required for MANY mode")
|
516
|
-
|
525
|
+
if isinstance(params, dict):
|
526
|
+
rows = await conn.fetch(query, params)
|
527
|
+
else:
|
528
|
+
rows = await conn.fetch(query, *params)
|
517
529
|
return [self._convert_row(dict(row)) for row in rows[:fetch_size]]
|
518
530
|
elif fetch_mode == FetchMode.ITERATOR:
|
519
531
|
raise NotImplementedError("Iterator mode not yet implemented")
|
@@ -532,7 +544,10 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
532
544
|
and "RETURNING" not in query_upper
|
533
545
|
and fetch_mode == FetchMode.ALL
|
534
546
|
):
|
535
|
-
|
547
|
+
if isinstance(params, dict):
|
548
|
+
result = await conn.execute(query, params)
|
549
|
+
else:
|
550
|
+
result = await conn.execute(query, *params)
|
536
551
|
# asyncpg returns a string like "UPDATE 1", extract the count
|
537
552
|
if isinstance(result, str):
|
538
553
|
parts = result.split()
|
@@ -544,15 +559,24 @@ class PostgreSQLAdapter(DatabaseAdapter):
|
|
544
559
|
return []
|
545
560
|
|
546
561
|
if fetch_mode == FetchMode.ONE:
|
547
|
-
|
562
|
+
if isinstance(params, dict):
|
563
|
+
row = await conn.fetchrow(query, params)
|
564
|
+
else:
|
565
|
+
row = await conn.fetchrow(query, *params)
|
548
566
|
return self._convert_row(dict(row)) if row else None
|
549
567
|
elif fetch_mode == FetchMode.ALL:
|
550
|
-
|
568
|
+
if isinstance(params, dict):
|
569
|
+
rows = await conn.fetch(query, params)
|
570
|
+
else:
|
571
|
+
rows = await conn.fetch(query, *params)
|
551
572
|
return [self._convert_row(dict(row)) for row in rows]
|
552
573
|
elif fetch_mode == FetchMode.MANY:
|
553
574
|
if not fetch_size:
|
554
575
|
raise ValueError("fetch_size required for MANY mode")
|
555
|
-
|
576
|
+
if isinstance(params, dict):
|
577
|
+
rows = await conn.fetch(query, params)
|
578
|
+
else:
|
579
|
+
rows = await conn.fetch(query, *params)
|
556
580
|
return [self._convert_row(dict(row)) for row in rows[:fetch_size]]
|
557
581
|
elif fetch_mode == FetchMode.ITERATOR:
|
558
582
|
raise NotImplementedError("Iterator mode not yet implemented")
|
@@ -1,4 +1,4 @@
|
|
1
|
-
kailash/__init__.py,sha256=
|
1
|
+
kailash/__init__.py,sha256=3qFmWmSZ0m_IFcnY0f-wmS3lPf-tvseGVfzBcQbCuq4,2771
|
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
|
@@ -83,7 +83,7 @@ kailash/mcp_server/__init__.py,sha256=AzrCEat5gdl9Nes8xOs7D4Wj3HpGlms3xLbOrx2diP
|
|
83
83
|
kailash/mcp_server/advanced_features.py,sha256=76dmttUa0M61ReBbgexf7Igu4CXaXS-CUmFhvTDjKyI,30673
|
84
84
|
kailash/mcp_server/ai_registry_server.py,sha256=vMNMvWLegKBVp7YAHVKgltWa2vTXKNvV-_Ni_z1argM,28973
|
85
85
|
kailash/mcp_server/auth.py,sha256=Y2BztTL2IILCLSHzZIDSXsIKyUTR8gUXwum5rKffUxs,25251
|
86
|
-
kailash/mcp_server/client.py,sha256=
|
86
|
+
kailash/mcp_server/client.py,sha256=CRb9BJgipgzEaxsC5ohx8JnBZQ8vEn0bLzvigQ5OhVg,39778
|
87
87
|
kailash/mcp_server/client_new.py,sha256=YU671JvAM0uvuX0uhGZCIKI8co3fqz0cs6HqLZ59Xyo,10285
|
88
88
|
kailash/mcp_server/discovery.py,sha256=D8vcwVkbgQCNp0_BlkGeU_dnqgIXN2g0s3_GpndlQu0,57828
|
89
89
|
kailash/mcp_server/errors.py,sha256=_lycwudWP_AJ_KwLO5N3VCKbG1ikfaTyzA2PBF8UAYU,21181
|
@@ -201,7 +201,7 @@ kailash/nodes/compliance/data_retention.py,sha256=90bH_eGwlcDzUdklAJeXQM-RcuLUGQ
|
|
201
201
|
kailash/nodes/compliance/gdpr.py,sha256=ZMoHZjAo4QtGwtFCzGMrAUBFV3TbZOnJ5DZGZS87Bas,70548
|
202
202
|
kailash/nodes/data/__init__.py,sha256=f0h4ysvXxlyFcNJLvDyXrgJ0ixwDF1cS0pJ2QNPakhg,5213
|
203
203
|
kailash/nodes/data/async_connection.py,sha256=wfArHs9svU48bxGZIiixSV2YVn9cukNgEjagwTRu6J4,17250
|
204
|
-
kailash/nodes/data/async_sql.py,sha256=
|
204
|
+
kailash/nodes/data/async_sql.py,sha256=yKwX4_gFc_Qi6sCF134XCgtERyIhAZKK7DomYSZG3bo,105201
|
205
205
|
kailash/nodes/data/async_vector.py,sha256=HtwQLO25IXu8Vq80qzU8rMkUAKPQ2qM0x8YxjXHlygU,21005
|
206
206
|
kailash/nodes/data/bulk_operations.py,sha256=WVopmosVkIlweFxVt3boLdCPc93EqpYyQ1Ez9mCIt0c,34453
|
207
207
|
kailash/nodes/data/directory.py,sha256=fbfLqD_ijRubk-4xew3604QntPsyDxqaF4k6TpfyjDg,9923
|
@@ -395,9 +395,9 @@ kailash/workflow/templates.py,sha256=XQMAKZXC2dlxgMMQhSEOWAF3hIbe9JJt9j_THchhAm8
|
|
395
395
|
kailash/workflow/type_inference.py,sha256=i1F7Yd_Z3elTXrthsLpqGbOnQBIVVVEjhRpI0HrIjd0,24492
|
396
396
|
kailash/workflow/validation.py,sha256=r2zApGiiG8UEn7p5Ji842l8OR1_KftzDkWc7gg0cac0,44675
|
397
397
|
kailash/workflow/visualization.py,sha256=nHBW-Ai8QBMZtn2Nf3EE1_aiMGi9S6Ui_BfpA5KbJPU,23187
|
398
|
-
kailash-0.9.
|
399
|
-
kailash-0.9.
|
400
|
-
kailash-0.9.
|
401
|
-
kailash-0.9.
|
402
|
-
kailash-0.9.
|
403
|
-
kailash-0.9.
|
398
|
+
kailash-0.9.2.dist-info/licenses/LICENSE,sha256=Axe6g7bTrJkToK9h9j2SpRUKKNaDZDCo2lQ2zPxCE6s,1065
|
399
|
+
kailash-0.9.2.dist-info/METADATA,sha256=ODgNfQBBaU7gn4dr4a_qP-NuxVuKE_B-QeSPkTEuJMw,21733
|
400
|
+
kailash-0.9.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
401
|
+
kailash-0.9.2.dist-info/entry_points.txt,sha256=M_q3b8PG5W4XbhSgESzIJjh3_4OBKtZFYFsOdkr2vO4,45
|
402
|
+
kailash-0.9.2.dist-info/top_level.txt,sha256=z7GzH2mxl66498pVf5HKwo5wwfPtt9Aq95uZUpH6JV0,8
|
403
|
+
kailash-0.9.2.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|