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.
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/PKG-INFO +6 -1
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/README.md +5 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/command_handler.py +3 -1
- paraview_mcp_python-0.1.3/bridge/gui_bridge.py +264 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/pyproject.toml +1 -1
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/start_paraview_gui_bridge.py +15 -6
- paraview_mcp_python-0.1.3/tests/test_gui_bridge.py +143 -0
- paraview_mcp_python-0.1.2/bridge/gui_bridge.py +0 -55
- paraview_mcp_python-0.1.2/tests/test_gui_bridge.py +0 -55
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/.gitignore +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/LICENSE +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/__init__.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/execution.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/models.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/bridge/server.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/docs/architecture.md +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/docs/python-execute-design.md +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/color_by.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/create_contour.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/create_slice.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/open_dataset.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/reset_camera.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/library/save_screenshot.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/paraview_bridge_request.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/start_paraview_bridge.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/src/paraview_mcp_server/__init__.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/src/paraview_mcp_server/headless.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/src/paraview_mcp_server/server.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/__init__.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_bridge_server.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_command_handler.py +0 -0
- {paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/tests/test_protocol.py +0 -0
- {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.
|
|
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
|
-
|
|
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}
|
{paraview_mcp_python-0.1.2 → paraview_mcp_python-0.1.3}/scripts/start_paraview_gui_bridge.py
RENAMED
|
@@ -4,9 +4,8 @@ Run from ParaView:
|
|
|
4
4
|
|
|
5
5
|
Tools -> Python Shell -> Run Script
|
|
6
6
|
|
|
7
|
-
Select this file. The script
|
|
8
|
-
returns immediately, so
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|