paraview-mcp-python 0.1.2__tar.gz → 0.1.3__tar.gz

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.
Files changed (33) hide show
  1. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/PKG-INFO +6 -1
  2. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/README.md +5 -0
  3. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/command_handler.py +3 -1
  4. paraview_mcp_python-0.1.3/bridge/gui_bridge.py +264 -0
  5. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/pyproject.toml +1 -1
  6. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/start_paraview_gui_bridge.py +15 -6
  7. paraview_mcp_python-0.1.3/tests/test_gui_bridge.py +143 -0
  8. paraview_mcp_python-0.1.2/bridge/gui_bridge.py +0 -55
  9. paraview_mcp_python-0.1.2/tests/test_gui_bridge.py +0 -55
  10. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/.gitignore +0 -0
  11. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/LICENSE +0 -0
  12. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/__init__.py +0 -0
  13. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/execution.py +0 -0
  14. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/models.py +0 -0
  15. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/server.py +0 -0
  16. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/docs/architecture.md +0 -0
  17. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/docs/python-execute-design.md +0 -0
  18. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/color_by.py +0 -0
  19. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/create_contour.py +0 -0
  20. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/create_slice.py +0 -0
  21. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/open_dataset.py +0 -0
  22. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/reset_camera.py +0 -0
  23. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/save_screenshot.py +0 -0
  24. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/paraview_bridge_request.py +0 -0
  25. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/start_paraview_bridge.py +0 -0
  26. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/src/paraview_mcp_server/__init__.py +0 -0
  27. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/src/paraview_mcp_server/headless.py +0 -0
  28. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/src/paraview_mcp_server/server.py +0 -0
  29. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/__init__.py +0 -0
  30. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_bridge_server.py +0 -0
  31. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_command_handler.py +0 -0
  32. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_protocol.py +0 -0
  33. {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: paraview-mcp-python
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: MCP server for controlling ParaView via AI assistants
5
5
  Project-URL: Homepage, https://github.com/djeada/paraview-mcp-server
6
6
  Project-URL: Repository, https://github.com/djeada/paraview-mcp-server
@@ -163,6 +163,11 @@ Expected output:
163
163
  ParaView MCP GUI bridge started on 127.0.0.1:9876
164
164
  ```
165
165
 
166
+ Do not start the live GUI bridge with `paraview --script
167
+ scripts/start_paraview_gui_bridge.py`. ParaView runs startup scripts before the
168
+ embedded GUI Python environment is fully ready for pipeline edits. Use **Tools
169
+ -> Python Shell -> Run Script** for the live GUI bridge.
170
+
166
171
  If `paraview-mcp-python` is installed into ParaView's Python environment, you
167
172
  can also start the live GUI bridge directly from the Python Shell:
168
173
 
@@ -130,6 +130,11 @@ Expected output:
130
130
  ParaView MCP GUI bridge started on 127.0.0.1:9876
131
131
  ```
132
132
 
133
+ Do not start the live GUI bridge with `paraview --script
134
+ scripts/start_paraview_gui_bridge.py`. ParaView runs startup scripts before the
135
+ embedded GUI Python environment is fully ready for pipeline edits. Use **Tools
136
+ -> Python Shell -> Run Script** for the live GUI bridge.
137
+
133
138
  If `paraview-mcp-python` is installed into ParaView's Python environment, you
134
139
  can also start the live GUI bridge directly from the Python Shell:
135
140
 
@@ -8,6 +8,7 @@ is available.
8
8
  from __future__ import annotations
9
9
 
10
10
  import logging
11
+ import os
11
12
  from typing import TYPE_CHECKING, Any
12
13
 
13
14
  from bridge.models import (
@@ -346,7 +347,8 @@ class CommandHandler:
346
347
  camera.SetViewUp(*params["view_up"])
347
348
  if "parallel_scale" in params:
348
349
  camera.SetParallelScale(params["parallel_scale"])
349
- view.StillRender()
350
+ if os.environ.get("PARAVIEW_MCP_GUI_BRIDGE") != "1":
351
+ view.StillRender()
350
352
  pos = list(camera.GetPosition())
351
353
  fp = list(camera.GetFocalPoint())
352
354
  up = list(camera.GetViewUp())
@@ -0,0 +1,264 @@
1
+ """Helpers for starting the bridge from an already-open ParaView GUI session."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import logging
8
+ import os
9
+ import select
10
+ import socket
11
+ import traceback
12
+ import uuid
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ from bridge.server import BUFFER_SIZE, HOST, PORT
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ @dataclass
22
+ class _ClientState:
23
+ sock: socket.socket
24
+ buffer: bytes = b""
25
+
26
+
27
+ class ParaViewGuiBridgeServer:
28
+ """Nonblocking TCP bridge polled from ParaView's GUI event loop."""
29
+
30
+ def __init__(self, host: str = HOST, port: int = PORT, poll_interval_ms: int = 50):
31
+ self._host = host
32
+ self._port = port
33
+ self._poll_interval_ms = poll_interval_ms
34
+ self._server_socket: socket.socket | None = None
35
+ self._clients: dict[socket.socket, _ClientState] = {}
36
+ self._running = False
37
+ self._interactor: Any | None = None
38
+ self._observer_id: int | None = None
39
+ self._timer_id: int | None = None
40
+ # Import here so this module can be imported without ParaView installed.
41
+ from bridge.command_handler import CommandHandler
42
+
43
+ self._handler = CommandHandler()
44
+
45
+ @property
46
+ def host(self) -> str:
47
+ return self._host
48
+
49
+ @property
50
+ def port(self) -> int:
51
+ return self._port
52
+
53
+ @property
54
+ def is_running(self) -> bool:
55
+ return self._running
56
+
57
+ def start(self) -> None:
58
+ if self._running:
59
+ return
60
+ self._interactor = self._get_render_window_interactor()
61
+ if not callable(getattr(self._interactor, "AddObserver", None)):
62
+ raise RuntimeError("ParaView render window interactor does not support VTK observers")
63
+ if not callable(getattr(self._interactor, "CreateRepeatingTimer", None)):
64
+ raise RuntimeError("ParaView render window interactor does not support repeating timers")
65
+
66
+ self._server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
67
+ self._server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
68
+ self._server_socket.setblocking(False)
69
+ self._server_socket.bind((self._host, self._port))
70
+ self._host, self._port = self._server_socket.getsockname()[:2]
71
+ self._server_socket.listen(5)
72
+
73
+ self._observer_id = self._interactor.AddObserver("TimerEvent", self._on_timer)
74
+ self._timer_id = self._interactor.CreateRepeatingTimer(self._poll_interval_ms)
75
+ os.environ["PARAVIEW_MCP_GUI_BRIDGE"] = "1"
76
+ self._running = True
77
+ logger.info("ParaView GUI bridge listening on %s:%s", self._host, self._port)
78
+
79
+ def stop(self) -> None:
80
+ self._running = False
81
+ if self._interactor is not None and self._timer_id is not None:
82
+ destroy_timer = getattr(self._interactor, "DestroyTimer", None)
83
+ if callable(destroy_timer):
84
+ with contextlib.suppress(Exception):
85
+ destroy_timer(self._timer_id)
86
+ if self._interactor is not None and self._observer_id is not None:
87
+ remove_observer = getattr(self._interactor, "RemoveObserver", None)
88
+ if callable(remove_observer):
89
+ with contextlib.suppress(Exception):
90
+ remove_observer(self._observer_id)
91
+ self._timer_id = None
92
+ self._observer_id = None
93
+ self._interactor = None
94
+ os.environ.pop("PARAVIEW_MCP_GUI_BRIDGE", None)
95
+
96
+ for client in list(self._clients):
97
+ self._close_client(client)
98
+ if self._server_socket is not None:
99
+ with contextlib.suppress(OSError):
100
+ self._server_socket.close()
101
+ self._server_socket = None
102
+
103
+ def _on_timer(self, _obj: Any, _event: str) -> None:
104
+ self.poll()
105
+
106
+ def poll(self) -> None:
107
+ """Process pending socket work without blocking the GUI event loop."""
108
+ if not self._running or self._server_socket is None:
109
+ return
110
+ sockets = [self._server_socket, *self._clients]
111
+ try:
112
+ readable, _, errored = select.select(sockets, [], sockets, 0)
113
+ except OSError:
114
+ return
115
+
116
+ for sock in errored:
117
+ if sock is self._server_socket:
118
+ logger.error("ParaView GUI bridge server socket failed")
119
+ self.stop()
120
+ return
121
+ self._close_client(sock)
122
+
123
+ for sock in readable:
124
+ if sock is self._server_socket:
125
+ self._accept_ready_clients()
126
+ return
127
+ else:
128
+ if self._read_client(sock):
129
+ return
130
+
131
+ def _accept_ready_clients(self) -> None:
132
+ if self._server_socket is None:
133
+ return
134
+ while True:
135
+ try:
136
+ conn, addr = self._server_socket.accept()
137
+ except BlockingIOError:
138
+ break
139
+ except OSError:
140
+ break
141
+ conn.setblocking(False)
142
+ self._clients[conn] = _ClientState(conn)
143
+ logger.info("Client connected from %s", addr)
144
+
145
+ def _read_client(self, sock: socket.socket) -> bool:
146
+ state = self._clients.get(sock)
147
+ if state is None:
148
+ return False
149
+ try:
150
+ data = sock.recv(BUFFER_SIZE)
151
+ except BlockingIOError:
152
+ return False
153
+ except OSError:
154
+ self._close_client(sock)
155
+ return False
156
+ if not data:
157
+ self._close_client(sock)
158
+ return False
159
+
160
+ state.buffer += data
161
+ while b"\n" in state.buffer:
162
+ line, state.buffer = state.buffer.split(b"\n", 1)
163
+ if not line.strip():
164
+ continue
165
+ try:
166
+ request = json.loads(line.decode("utf-8"))
167
+ response = self._process_request(request)
168
+ except Exception as exc:
169
+ response = {"id": None, "success": False, "error": str(exc)}
170
+ self._send_response(sock, response)
171
+ return True
172
+ return False
173
+
174
+ def _send_response(self, sock: socket.socket, response: dict[str, Any]) -> None:
175
+ try:
176
+ sock.sendall((json.dumps(response) + "\n").encode("utf-8"))
177
+ except OSError:
178
+ self._close_client(sock)
179
+
180
+ def _close_client(self, sock: socket.socket) -> None:
181
+ self._clients.pop(sock, None)
182
+ with contextlib.suppress(OSError):
183
+ sock.close()
184
+
185
+ def _process_request(self, request: dict[str, Any]) -> dict[str, Any]:
186
+ if not isinstance(request, dict):
187
+ raise TypeError("Request must be a JSON object")
188
+
189
+ req_id = request.get("id", str(uuid.uuid4()))
190
+ command = request.get("command")
191
+ params = request.get("params", {})
192
+ if not isinstance(command, str) or not command.strip():
193
+ return {"id": req_id, "success": False, "error": "Missing or invalid command"}
194
+ if not isinstance(params, dict):
195
+ return {"id": req_id, "success": False, "error": "Invalid params: expected JSON object"}
196
+ try:
197
+ result = self._handler.handle(command, params)
198
+ return {"id": req_id, "success": True, "result": result}
199
+ except Exception as exc:
200
+ logger.error("Command '%s' failed: %s\n%s", command, exc, traceback.format_exc())
201
+ return {"id": req_id, "success": False, "error": str(exc)}
202
+
203
+ @staticmethod
204
+ def _get_render_window_interactor() -> Any:
205
+ import paraview # noqa: PLC0415
206
+ import paraview.simple as pvs # noqa: PLC0415
207
+
208
+ if not getattr(paraview, "fromGUI", False):
209
+ raise RuntimeError(
210
+ "The live GUI bridge must be started from ParaView's Python Shell with Run Script. "
211
+ "Starting it with 'paraview --script' runs too early in ParaView startup and is not stable."
212
+ )
213
+
214
+ view = pvs.GetActiveViewOrCreate("RenderView")
215
+ render_window = view.GetRenderWindow()
216
+ interactor = render_window.GetInteractor()
217
+ if interactor is None:
218
+ raise RuntimeError("ParaView render window interactor is not available")
219
+ return interactor
220
+
221
+
222
+ _SERVER: ParaViewGuiBridgeServer | None = None
223
+
224
+
225
+ def start_gui_bridge(host: str = HOST, port: int = PORT) -> dict[str, Any]:
226
+ """Start the bridge inside the current ParaView GUI process."""
227
+ global _SERVER
228
+ if _SERVER is not None and _SERVER.is_running:
229
+ return {
230
+ "host": _SERVER.host,
231
+ "port": _SERVER.port,
232
+ "running": True,
233
+ "already_running": True,
234
+ }
235
+
236
+ server = ParaViewGuiBridgeServer(host=host, port=port)
237
+ server.start()
238
+ _SERVER = server
239
+ return {
240
+ "host": server.host,
241
+ "port": server.port,
242
+ "running": True,
243
+ "already_running": False,
244
+ }
245
+
246
+
247
+ def stop_gui_bridge() -> dict[str, Any]:
248
+ """Stop the bridge started by :func:`start_gui_bridge`."""
249
+ global _SERVER
250
+ if _SERVER is None:
251
+ return {"running": False, "stopped": False}
252
+
253
+ host = _SERVER.host
254
+ port = _SERVER.port
255
+ _SERVER.stop()
256
+ _SERVER = None
257
+ return {"host": host, "port": port, "running": False, "stopped": True}
258
+
259
+
260
+ def gui_bridge_status() -> dict[str, Any]:
261
+ """Return the current GUI bridge status."""
262
+ if _SERVER is None or not _SERVER.is_running:
263
+ return {"running": False}
264
+ return {"host": _SERVER.host, "port": _SERVER.port, "running": True}
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "paraview-mcp-python"
7
- version = "0.1.2"
7
+ version = "0.1.3"
8
8
  description = "MCP server for controlling ParaView via AI assistants"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -4,9 +4,8 @@ Run from ParaView:
4
4
 
5
5
  Tools -> Python Shell -> Run Script
6
6
 
7
- Select this file. The script starts the TCP bridge in a background thread and
8
- returns immediately, so the ParaView GUI remains usable. MCP commands will then
9
- modify this open ParaView session.
7
+ Select this file. The script attaches the TCP bridge to ParaView's GUI event
8
+ loop and returns immediately, so MCP commands modify this open ParaView session.
10
9
  """
11
10
 
12
11
  from __future__ import annotations
@@ -14,12 +13,22 @@ from __future__ import annotations
14
13
  import logging
15
14
  import os
16
15
  import sys
16
+ from pathlib import Path
17
17
 
18
18
  logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s")
19
19
 
20
- REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
- if REPO_ROOT not in sys.path:
22
- sys.path.insert(0, REPO_ROOT)
20
+ SCRIPT_PATH = globals().get("__file__")
21
+ ROOT_CANDIDATES = []
22
+ if SCRIPT_PATH:
23
+ ROOT_CANDIDATES.append(Path(SCRIPT_PATH).resolve().parents[1])
24
+ ROOT_CANDIDATES.extend([Path.cwd(), Path.cwd().parent])
25
+
26
+ for candidate in ROOT_CANDIDATES:
27
+ if (candidate / "bridge" / "gui_bridge.py").is_file():
28
+ repo_root = str(candidate)
29
+ if repo_root not in sys.path:
30
+ sys.path.insert(0, repo_root)
31
+ break
23
32
 
24
33
  from bridge.gui_bridge import gui_bridge_status, start_gui_bridge, stop_gui_bridge # noqa: E402
25
34
 
@@ -0,0 +1,143 @@
1
+ """Tests for the ParaView GUI bridge lifecycle helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import socket
7
+ import sys
8
+ from types import ModuleType
9
+ from unittest.mock import MagicMock, patch
10
+
11
+ import pytest
12
+
13
+ from bridge import gui_bridge
14
+
15
+
16
+ class FakeInteractor:
17
+ def __init__(self):
18
+ self.callback = None
19
+ self.removed_observer = None
20
+ self.destroyed_timer = None
21
+
22
+ def AddObserver(self, event_name, callback): # noqa: N802
23
+ assert event_name == "TimerEvent"
24
+ self.callback = callback
25
+ return 100
26
+
27
+ def CreateRepeatingTimer(self, interval_ms): # noqa: N802
28
+ assert interval_ms > 0
29
+ return 200
30
+
31
+ def DestroyTimer(self, timer_id): # noqa: N802
32
+ self.destroyed_timer = timer_id
33
+
34
+ def RemoveObserver(self, observer_id): # noqa: N802
35
+ self.removed_observer = observer_id
36
+
37
+
38
+ class FakeRenderWindow:
39
+ def __init__(self, interactor):
40
+ self._interactor = interactor
41
+
42
+ def GetInteractor(self): # noqa: N802
43
+ return self._interactor
44
+
45
+
46
+ class FakeView:
47
+ def __init__(self, interactor):
48
+ self._interactor = interactor
49
+
50
+ def GetRenderWindow(self): # noqa: N802
51
+ return FakeRenderWindow(self._interactor)
52
+
53
+
54
+ @pytest.fixture
55
+ def fake_interactor(monkeypatch):
56
+ interactor = FakeInteractor()
57
+ paraview_module = ModuleType("paraview")
58
+ paraview_module.fromGUI = True
59
+ simple_module = ModuleType("paraview.simple")
60
+ simple_module.GetActiveViewOrCreate = MagicMock(return_value=FakeView(interactor))
61
+ monkeypatch.setitem(sys.modules, "paraview", paraview_module)
62
+ monkeypatch.setitem(sys.modules, "paraview.simple", simple_module)
63
+ yield interactor
64
+ gui_bridge.stop_gui_bridge()
65
+
66
+
67
+ def test_start_gui_bridge_is_non_blocking_and_reports_status(fake_interactor):
68
+ with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
69
+ status = gui_bridge.start_gui_bridge(port=0)
70
+
71
+ assert fake_interactor.callback is not None
72
+ assert status["running"] is True
73
+ assert status["already_running"] is False
74
+ assert status["host"] == "127.0.0.1"
75
+ assert status["port"] > 0
76
+ assert gui_bridge.gui_bridge_status() == {
77
+ "host": status["host"],
78
+ "port": status["port"],
79
+ "running": True,
80
+ }
81
+
82
+
83
+ def test_start_gui_bridge_is_idempotent(fake_interactor):
84
+ with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
85
+ first = gui_bridge.start_gui_bridge(port=0)
86
+ second = gui_bridge.start_gui_bridge(port=0)
87
+
88
+ assert second == {
89
+ "host": first["host"],
90
+ "port": first["port"],
91
+ "running": True,
92
+ "already_running": True,
93
+ }
94
+
95
+
96
+ def test_stop_gui_bridge_stops_running_server(fake_interactor):
97
+ with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
98
+ started = gui_bridge.start_gui_bridge(port=0)
99
+
100
+ stopped = gui_bridge.stop_gui_bridge()
101
+
102
+ assert stopped == {
103
+ "host": started["host"],
104
+ "port": started["port"],
105
+ "running": False,
106
+ "stopped": True,
107
+ }
108
+ assert fake_interactor.destroyed_timer == 200
109
+ assert fake_interactor.removed_observer == 100
110
+ assert gui_bridge.gui_bridge_status() == {"running": False}
111
+
112
+
113
+ def test_gui_bridge_processes_socket_request_from_poll_callback(fake_interactor):
114
+ handler = MagicMock()
115
+ handler.handle.return_value = {"source_count": 0}
116
+ with patch("bridge.command_handler.CommandHandler", return_value=handler):
117
+ started = gui_bridge.start_gui_bridge(port=0)
118
+
119
+ with socket.create_connection((started["host"], started["port"]), timeout=1) as client:
120
+ request = {"id": "abc", "command": "scene.get_info", "params": {}}
121
+ client.sendall((json.dumps(request) + "\n").encode("utf-8"))
122
+ assert fake_interactor.callback is not None
123
+ fake_interactor.callback(None, "TimerEvent")
124
+ fake_interactor.callback(None, "TimerEvent")
125
+ response = client.recv(65536)
126
+
127
+ assert json.loads(response.decode("utf-8")) == {
128
+ "id": "abc",
129
+ "success": True,
130
+ "result": {"source_count": 0},
131
+ }
132
+ handler.handle.assert_called_once_with("scene.get_info", {})
133
+
134
+
135
+ def test_gui_bridge_rejects_command_line_startup(monkeypatch):
136
+ paraview_module = ModuleType("paraview")
137
+ paraview_module.fromGUI = False
138
+ simple_module = ModuleType("paraview.simple")
139
+ monkeypatch.setitem(sys.modules, "paraview", paraview_module)
140
+ monkeypatch.setitem(sys.modules, "paraview.simple", simple_module)
141
+
142
+ with pytest.raises(RuntimeError, match="Python Shell"):
143
+ gui_bridge.start_gui_bridge(port=0)
@@ -1,55 +0,0 @@
1
- """Helpers for starting the bridge from an already-open ParaView GUI session."""
2
-
3
- from __future__ import annotations
4
-
5
- from typing import Any
6
-
7
- from bridge.server import HOST, PORT, ParaViewBridgeServer
8
-
9
- _SERVER: ParaViewBridgeServer | None = None
10
-
11
-
12
- def start_gui_bridge(host: str = HOST, port: int = PORT) -> dict[str, Any]:
13
- """Start the bridge inside the current ParaView Python process.
14
-
15
- This function is intentionally non-blocking so it can be called from the
16
- ParaView GUI Python shell without freezing the application.
17
- """
18
- global _SERVER
19
- if _SERVER is not None and _SERVER.is_running:
20
- return {
21
- "host": _SERVER.host,
22
- "port": _SERVER.port,
23
- "running": True,
24
- "already_running": True,
25
- }
26
-
27
- server = ParaViewBridgeServer(host=host, port=port)
28
- server.start()
29
- _SERVER = server
30
- return {
31
- "host": server.host,
32
- "port": server.port,
33
- "running": True,
34
- "already_running": False,
35
- }
36
-
37
-
38
- def stop_gui_bridge() -> dict[str, Any]:
39
- """Stop the bridge started by :func:`start_gui_bridge`."""
40
- global _SERVER
41
- if _SERVER is None:
42
- return {"running": False, "stopped": False}
43
-
44
- host = _SERVER.host
45
- port = _SERVER.port
46
- _SERVER.stop()
47
- _SERVER = None
48
- return {"host": host, "port": port, "running": False, "stopped": True}
49
-
50
-
51
- def gui_bridge_status() -> dict[str, Any]:
52
- """Return the current GUI bridge status."""
53
- if _SERVER is None or not _SERVER.is_running:
54
- return {"running": False}
55
- return {"host": _SERVER.host, "port": _SERVER.port, "running": True}
@@ -1,55 +0,0 @@
1
- """Tests for the ParaView GUI bridge lifecycle helpers."""
2
-
3
- from __future__ import annotations
4
-
5
- from unittest.mock import MagicMock, patch
6
-
7
- from bridge import gui_bridge
8
-
9
-
10
- def teardown_function(_function):
11
- gui_bridge.stop_gui_bridge()
12
-
13
-
14
- def test_start_gui_bridge_is_non_blocking_and_reports_status():
15
- with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
16
- status = gui_bridge.start_gui_bridge(port=0)
17
-
18
- assert status["running"] is True
19
- assert status["already_running"] is False
20
- assert status["host"] == "127.0.0.1"
21
- assert status["port"] > 0
22
- assert gui_bridge.gui_bridge_status() == {
23
- "host": status["host"],
24
- "port": status["port"],
25
- "running": True,
26
- }
27
-
28
-
29
- def test_start_gui_bridge_is_idempotent():
30
- with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
31
- first = gui_bridge.start_gui_bridge(port=0)
32
- second = gui_bridge.start_gui_bridge(port=0)
33
-
34
- assert first["running"] is True
35
- assert second == {
36
- "host": first["host"],
37
- "port": first["port"],
38
- "running": True,
39
- "already_running": True,
40
- }
41
-
42
-
43
- def test_stop_gui_bridge_stops_running_server():
44
- with patch("bridge.command_handler.CommandHandler", return_value=MagicMock()):
45
- started = gui_bridge.start_gui_bridge(port=0)
46
-
47
- stopped = gui_bridge.stop_gui_bridge()
48
-
49
- assert stopped == {
50
- "host": started["host"],
51
- "port": started["port"],
52
- "running": False,
53
- "stopped": True,
54
- }
55
- assert gui_bridge.gui_bridge_status() == {"running": False}