Merge branch 'main' into multiuser
# Conflicts: # connpy/grpc_layer/server.py
This commit is contained in:
@@ -14,6 +14,23 @@ class NodeHandler:
|
|||||||
self.app = app
|
self.app = app
|
||||||
self.forms = Forms(app)
|
self.forms = Forms(app)
|
||||||
|
|
||||||
|
def _filter_exact_match(self, matches, query):
|
||||||
|
if not query or len(matches) <= 1:
|
||||||
|
return matches
|
||||||
|
|
||||||
|
exact_matches = []
|
||||||
|
for m in matches:
|
||||||
|
if self.app.case:
|
||||||
|
if m == query:
|
||||||
|
exact_matches.append(m)
|
||||||
|
else:
|
||||||
|
if m.lower() == query.lower():
|
||||||
|
exact_matches.append(m)
|
||||||
|
|
||||||
|
if len(exact_matches) == 1:
|
||||||
|
return exact_matches
|
||||||
|
return matches
|
||||||
|
|
||||||
def dispatch(self, args):
|
def dispatch(self, args):
|
||||||
if not self.app.case and args.data != None:
|
if not self.app.case and args.data != None:
|
||||||
args.data = args.data.lower()
|
args.data = args.data.lower()
|
||||||
@@ -39,6 +56,7 @@ class NodeHandler:
|
|||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
matches = self.app.services.nodes.list_nodes(args.data)
|
matches = self.app.services.nodes.list_nodes(args.data)
|
||||||
|
matches = self._filter_exact_match(matches, args.data)
|
||||||
except Exception:
|
except Exception:
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
@@ -73,6 +91,7 @@ class NodeHandler:
|
|||||||
matches = self.app.services.nodes.list_folders(args.data)
|
matches = self.app.services.nodes.list_folders(args.data)
|
||||||
else:
|
else:
|
||||||
matches = self.app.services.nodes.list_nodes(args.data)
|
matches = self.app.services.nodes.list_nodes(args.data)
|
||||||
|
matches = self._filter_exact_match(matches, args.data)
|
||||||
except Exception:
|
except Exception:
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
@@ -87,8 +106,9 @@ class NodeHandler:
|
|||||||
sys.exit(7)
|
sys.exit(7)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
for item in matches:
|
for i, item in enumerate(matches):
|
||||||
self.app.services.nodes.delete_node(item, is_folder=is_folder)
|
save_on_last = (i == len(matches) - 1)
|
||||||
|
self.app.services.nodes.delete_node(item, is_folder=is_folder, save=save_on_last)
|
||||||
|
|
||||||
if len(matches) == 1:
|
if len(matches) == 1:
|
||||||
printer.success(f"{matches[0]} deleted successfully")
|
printer.success(f"{matches[0]} deleted successfully")
|
||||||
@@ -144,6 +164,7 @@ class NodeHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
matches = self.app.services.nodes.list_nodes(args.data)
|
matches = self.app.services.nodes.list_nodes(args.data)
|
||||||
|
matches = self._filter_exact_match(matches, args.data)
|
||||||
except Exception:
|
except Exception:
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
@@ -171,6 +192,7 @@ class NodeHandler:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
matches = self.app.services.nodes.list_nodes(args.data)
|
matches = self.app.services.nodes.list_nodes(args.data)
|
||||||
|
matches = self._filter_exact_match(matches, args.data)
|
||||||
except Exception:
|
except Exception:
|
||||||
matches = []
|
matches = []
|
||||||
|
|
||||||
@@ -209,7 +231,7 @@ class NodeHandler:
|
|||||||
self.app.services.nodes.update_node(matches[0], updatenode)
|
self.app.services.nodes.update_node(matches[0], updatenode)
|
||||||
printer.success(f"{args.data} edited successfully")
|
printer.success(f"{args.data} edited successfully")
|
||||||
else:
|
else:
|
||||||
editcount = 0
|
changed_items = []
|
||||||
for k in matches:
|
for k in matches:
|
||||||
updated_item = self.app.services.nodes.explode_unique(k)
|
updated_item = self.app.services.nodes.explode_unique(k)
|
||||||
updated_item["type"] = "connection"
|
updated_item["type"] = "connection"
|
||||||
@@ -222,8 +244,12 @@ class NodeHandler:
|
|||||||
updated_item[key] = updatenode[key]
|
updated_item[key] = updatenode[key]
|
||||||
|
|
||||||
if this_item_changed:
|
if this_item_changed:
|
||||||
editcount += 1
|
changed_items.append((k, updated_item))
|
||||||
self.app.services.nodes.update_node(k, updated_item)
|
|
||||||
|
editcount = len(changed_items)
|
||||||
|
for i, (k, updated_item) in enumerate(changed_items):
|
||||||
|
save_on_last = (i == editcount - 1)
|
||||||
|
self.app.services.nodes.update_node(k, updated_item, save=save_on_last)
|
||||||
|
|
||||||
if editcount == 0:
|
if editcount == 0:
|
||||||
printer.info("Nothing to do here")
|
printer.info("Nothing to do here")
|
||||||
|
|||||||
@@ -928,12 +928,17 @@ class StatusBridge:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
||||||
def __init__(self, provider, registry=None):
|
def __init__(self, provider, registry=None, debug=False):
|
||||||
if not hasattr(provider, "mode"):
|
if not hasattr(provider, "mode"):
|
||||||
from connpy.services.provider import ServiceProvider
|
from connpy.services.provider import ServiceProvider
|
||||||
provider = ServiceProvider(provider, mode="local")
|
provider = ServiceProvider(provider, mode="local")
|
||||||
self._fallback_provider = provider
|
self._fallback_provider = provider
|
||||||
self._registry = registry
|
self._registry = registry
|
||||||
|
self.server_debug = debug
|
||||||
|
if debug:
|
||||||
|
from rich.console import Console
|
||||||
|
from ..printer import connpy_theme, get_original_stdout
|
||||||
|
self.server_console = Console(theme=connpy_theme, file=get_original_stdout())
|
||||||
|
|
||||||
def _get_provider(self):
|
def _get_provider(self):
|
||||||
if self._registry:
|
if self._registry:
|
||||||
@@ -988,6 +993,16 @@ class AIServicer(connpy_pb2_grpc.AIServiceServicer):
|
|||||||
|
|
||||||
# Send final chunk marker
|
# Send final chunk marker
|
||||||
chunk_queue.put(("final_mark", res))
|
chunk_queue.put(("final_mark", res))
|
||||||
|
except ValueError as e:
|
||||||
|
# Configuration or LLM provider connection errors are expected, only print in debug mode
|
||||||
|
if debug or getattr(self, "server_debug", False):
|
||||||
|
from rich.console import Console
|
||||||
|
from ..printer import connpy_theme, get_original_stdout
|
||||||
|
c = getattr(self, "server_console", None) or Console(theme=connpy_theme, file=get_original_stdout())
|
||||||
|
c.print(f"[debug][DEBUG][/debug] AI Task Error: {e}")
|
||||||
|
chunk_queue.put(("status", f"Error: {str(e)}"))
|
||||||
|
# Crucial: always send final_mark to avoid client deadlock
|
||||||
|
chunk_queue.put(("final_mark", {"response": f"Error: {str(e)}", "chat_history": history, "error": True}))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
print(f"AI Task Error: {e}")
|
print(f"AI Task Error: {e}")
|
||||||
@@ -1344,7 +1359,7 @@ def serve(config, port=8048, debug=False):
|
|||||||
remote_plugin_pb2_grpc.add_RemotePluginServiceServicer_to_server(plugin_servicer, server)
|
remote_plugin_pb2_grpc.add_RemotePluginServiceServicer_to_server(plugin_servicer, server)
|
||||||
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(ExecutionServicer(fallback_provider, registry=registry), server)
|
connpy_pb2_grpc.add_ExecutionServiceServicer_to_server(ExecutionServicer(fallback_provider, registry=registry), server)
|
||||||
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(ImportExportServicer(fallback_provider, registry=registry), server)
|
connpy_pb2_grpc.add_ImportExportServiceServicer_to_server(ImportExportServicer(fallback_provider, registry=registry), server)
|
||||||
connpy_pb2_grpc.add_AIServiceServicer_to_server(AIServicer(fallback_provider, registry=registry), server)
|
connpy_pb2_grpc.add_AIServiceServicer_to_server(AIServicer(fallback_provider, registry=registry, debug=debug), server)
|
||||||
connpy_pb2_grpc.add_SystemServiceServicer_to_server(SystemServicer(fallback_provider, registry=registry), server)
|
connpy_pb2_grpc.add_SystemServiceServicer_to_server(SystemServicer(fallback_provider, registry=registry), server)
|
||||||
connpy_pb2_grpc.add_AuthServiceServicer_to_server(AuthServicer(registry), server)
|
connpy_pb2_grpc.add_AuthServiceServicer_to_server(AuthServicer(registry), server)
|
||||||
|
|
||||||
|
|||||||
@@ -462,16 +462,18 @@ class NodeStub:
|
|||||||
self._trigger_local_cache_sync()
|
self._trigger_local_cache_sync()
|
||||||
|
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def update_node(self, unique_id, data):
|
def update_node(self, unique_id, data, save=True):
|
||||||
req = connpy_pb2.NodeRequest(id=unique_id, data=to_struct(data), is_folder=False)
|
req = connpy_pb2.NodeRequest(id=unique_id, data=to_struct(data), is_folder=False)
|
||||||
self.stub.update_node(req)
|
self.stub.update_node(req)
|
||||||
self._trigger_local_cache_sync()
|
if save:
|
||||||
|
self._trigger_local_cache_sync()
|
||||||
|
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def delete_node(self, unique_id, is_folder=False):
|
def delete_node(self, unique_id, is_folder=False, save=True):
|
||||||
req = connpy_pb2.DeleteRequest(id=unique_id, is_folder=is_folder)
|
req = connpy_pb2.DeleteRequest(id=unique_id, is_folder=is_folder)
|
||||||
self.stub.delete_node(req)
|
self.stub.delete_node(req)
|
||||||
self._trigger_local_cache_sync()
|
if save:
|
||||||
|
self._trigger_local_cache_sync()
|
||||||
|
|
||||||
@handle_errors
|
@handle_errors
|
||||||
def move_node(self, src_id, dst_id, copy=False):
|
def move_node(self, src_id, dst_id, copy=False):
|
||||||
@@ -895,9 +897,6 @@ class AIStub:
|
|||||||
from ..printer import connpy_theme, get_original_stdout
|
from ..printer import connpy_theme, get_original_stdout
|
||||||
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
stable_console = RichConsole(theme=connpy_theme, file=get_original_stdout())
|
||||||
stable_console.print(Rule(style=alias))
|
stable_console.print(Rule(style=alias))
|
||||||
elif not full_content and final_result.get("response"):
|
|
||||||
# If nothing streamed but we have response (e.g. error or direct guide)
|
|
||||||
printer.console.print(Panel(Markdown(final_result["response"]), title=title, border_style=alias, expand=False))
|
|
||||||
break
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# Check if it was a gRPC error that we should let handle_errors catch
|
# Check if it was a gRPC error that we should let handle_errors catch
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ class NodeService(BaseService):
|
|||||||
self.config._connections_add(**data)
|
self.config._connections_add(**data)
|
||||||
self.config._saveconfig(self.config.file)
|
self.config._saveconfig(self.config.file)
|
||||||
|
|
||||||
def update_node(self, unique_id, data):
|
def update_node(self, unique_id, data, save=True):
|
||||||
"""Explicitly update an existing node."""
|
"""Explicitly update an existing node."""
|
||||||
all_nodes = self.config._getallnodes()
|
all_nodes = self.config._getallnodes()
|
||||||
if unique_id not in all_nodes:
|
if unique_id not in all_nodes:
|
||||||
@@ -162,9 +162,10 @@ class NodeService(BaseService):
|
|||||||
|
|
||||||
# config._connections_add actually handles updates if ID exists correctly
|
# config._connections_add actually handles updates if ID exists correctly
|
||||||
self.config._connections_add(**data)
|
self.config._connections_add(**data)
|
||||||
self.config._saveconfig(self.config.file)
|
if save:
|
||||||
|
self.config._saveconfig(self.config.file)
|
||||||
|
|
||||||
def delete_node(self, unique_id, is_folder=False):
|
def delete_node(self, unique_id, is_folder=False, save=True):
|
||||||
"""Logic for deleting a node or folder."""
|
"""Logic for deleting a node or folder."""
|
||||||
if is_folder:
|
if is_folder:
|
||||||
uniques = self.config._explode_unique(unique_id)
|
uniques = self.config._explode_unique(unique_id)
|
||||||
@@ -177,7 +178,8 @@ class NodeService(BaseService):
|
|||||||
raise NodeNotFoundError(f"Node '{unique_id}' not found or invalid.")
|
raise NodeNotFoundError(f"Node '{unique_id}' not found or invalid.")
|
||||||
self.config._connections_del(**uniques)
|
self.config._connections_del(**uniques)
|
||||||
|
|
||||||
self.config._saveconfig(self.config.file)
|
if save:
|
||||||
|
self.config._saveconfig(self.config.file)
|
||||||
|
|
||||||
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
def connect_node(self, unique_id, sftp=False, debug=False, logger=None):
|
||||||
"""Interact with a node directly."""
|
"""Interact with a node directly."""
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def test_node_del(mock_prompt, mock_delete_node, mock_list_nodes, app):
|
|||||||
mock_list_nodes.return_value = ["router1"]
|
mock_list_nodes.return_value = ["router1"]
|
||||||
mock_prompt.return_value = {"delete": True}
|
mock_prompt.return_value = {"delete": True}
|
||||||
app.start(["node", "-r", "router1"])
|
app.start(["node", "-r", "router1"])
|
||||||
mock_delete_node.assert_called_once_with("router1", is_folder=False)
|
mock_delete_node.assert_called_once_with("router1", is_folder=False, save=True)
|
||||||
|
|
||||||
@patch("connpy.services.node_service.NodeService.list_nodes")
|
@patch("connpy.services.node_service.NodeService.list_nodes")
|
||||||
@patch("connpy.services.node_service.NodeService.get_node_details")
|
@patch("connpy.services.node_service.NodeService.get_node_details")
|
||||||
@@ -314,3 +314,13 @@ def test_config_auth_file_path(mock_get_settings, mock_update_setting, mock_open
|
|||||||
assert args[1]["engineer_auth"] == {"vertex_project": "file-project"}
|
assert args[1]["engineer_auth"] == {"vertex_project": "file-project"}
|
||||||
|
|
||||||
|
|
||||||
|
@patch("connpy.services.node_service.NodeService.list_nodes")
|
||||||
|
@patch("connpy.services.node_service.NodeService.connect_node")
|
||||||
|
def test_node_connect_exact_match_priority(mock_connect_node, mock_list_nodes, app):
|
||||||
|
"""Test that exact matches are prioritized over partial/regex matches when connecting."""
|
||||||
|
mock_list_nodes.return_value = ["pe1@ctx", "qro1pe1@ctx"]
|
||||||
|
app.start(["node", "pe1@ctx"])
|
||||||
|
mock_connect_node.assert_called_once_with("pe1@ctx", sftp=False, debug=False, logger=app._service_logger)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user