datalab-platform 1.0.4__py3-none-any.whl → 1.1.0__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.
Files changed (45) hide show
  1. datalab/__init__.py +1 -1
  2. datalab/config.py +4 -0
  3. datalab/control/baseproxy.py +160 -0
  4. datalab/control/remote.py +175 -1
  5. datalab/data/doc/DataLab_en.pdf +0 -0
  6. datalab/data/doc/DataLab_fr.pdf +0 -0
  7. datalab/data/icons/control/copy_connection_info.svg +11 -0
  8. datalab/data/icons/control/start_webapi_server.svg +19 -0
  9. datalab/data/icons/control/stop_webapi_server.svg +7 -0
  10. datalab/gui/main.py +221 -2
  11. datalab/gui/settings.py +10 -0
  12. datalab/gui/tour.py +2 -3
  13. datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
  14. datalab/locale/fr/LC_MESSAGES/datalab.po +87 -1
  15. datalab/tests/__init__.py +32 -1
  16. datalab/tests/backbone/config_unit_test.py +1 -1
  17. datalab/tests/backbone/main_app_test.py +4 -0
  18. datalab/tests/backbone/memory_leak.py +1 -1
  19. datalab/tests/features/common/createobject_unit_test.py +1 -1
  20. datalab/tests/features/common/misc_app_test.py +5 -0
  21. datalab/tests/features/control/call_method_unit_test.py +104 -0
  22. datalab/tests/features/control/embedded1_unit_test.py +8 -0
  23. datalab/tests/features/control/remoteclient_app_test.py +39 -35
  24. datalab/tests/features/control/simpleclient_unit_test.py +7 -3
  25. datalab/tests/features/hdf5/h5browser2_unit.py +1 -1
  26. datalab/tests/features/image/background_dialog_test.py +2 -2
  27. datalab/tests/features/image/imagetools_unit_test.py +1 -1
  28. datalab/tests/features/signal/baseline_dialog_test.py +1 -1
  29. datalab/tests/webapi_test.py +395 -0
  30. datalab/webapi/__init__.py +95 -0
  31. datalab/webapi/actions.py +318 -0
  32. datalab/webapi/adapter.py +642 -0
  33. datalab/webapi/controller.py +379 -0
  34. datalab/webapi/routes.py +576 -0
  35. datalab/webapi/schema.py +198 -0
  36. datalab/webapi/serialization.py +388 -0
  37. datalab/widgets/status.py +61 -0
  38. {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/METADATA +6 -2
  39. {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/RECORD +45 -33
  40. /datalab/data/icons/{libre-gui-link.svg → control/libre-gui-link.svg} +0 -0
  41. /datalab/data/icons/{libre-gui-unlink.svg → control/libre-gui-unlink.svg} +0 -0
  42. {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/WHEEL +0 -0
  43. {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/entry_points.txt +0 -0
  44. {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/licenses/LICENSE +0 -0
  45. {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,95 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause License
2
+ # See LICENSE file for details
3
+
4
+ """
5
+ DataLab Web API
6
+ ===============
7
+
8
+ This package provides a web-native HTTP/JSON API for DataLab, enabling:
9
+
10
+ - **DataLab-Kernel integration**: Jupyter notebooks can connect to a running
11
+ DataLab instance via HTTP instead of XML-RPC
12
+ - **WASM/Pyodide compatibility**: The HTTP-based protocol works in WebAssembly
13
+ environments where XML-RPC is not available
14
+ - **External tool integration**: Any HTTP client can interact with the DataLab
15
+ workspace
16
+
17
+ Architecture
18
+ ------------
19
+
20
+ The Web API follows a layered design:
21
+
22
+ - **Control plane (JSON)**: Metadata operations via standard REST endpoints
23
+ - **Data plane (binary)**: Efficient NumPy array transfer using NPZ format
24
+ - **Events (WebSocket)**: Optional real-time notifications (future)
25
+
26
+ Security
27
+ --------
28
+
29
+ By default, the API:
30
+
31
+ - Binds to localhost only (127.0.0.1)
32
+ - Requires a bearer token for authentication
33
+ - Token is generated at startup and displayed in the UI
34
+
35
+ Usage
36
+ -----
37
+
38
+ Enable the Web API via:
39
+
40
+ - **UI**: Tools → Web API → Start
41
+ - **CLI**: ``datalab --webapi``
42
+ - **Environment**: ``DATALAB_WEBAPI_ENABLED=1``
43
+
44
+ See Also
45
+ --------
46
+
47
+ - :mod:`datalab.webapi.controller`: Server lifecycle management
48
+ - :mod:`datalab.webapi.routes`: API endpoint definitions
49
+ - :mod:`datalab.webapi.adapter`: Thread-safe workspace access
50
+ """
51
+
52
+ from __future__ import annotations
53
+
54
+ __all__ = [
55
+ "WEBAPI_AVAILABLE",
56
+ "get_webapi_controller",
57
+ ]
58
+
59
+ # Check if webapi dependencies are available
60
+ try:
61
+ import fastapi # noqa: F401
62
+ import uvicorn # noqa: F401
63
+
64
+ WEBAPI_AVAILABLE = True
65
+ except ImportError:
66
+ WEBAPI_AVAILABLE = False
67
+
68
+ _CONTROLLER_INSTANCE = None
69
+
70
+
71
+ def get_webapi_controller():
72
+ """Get the singleton WebAPI controller instance.
73
+
74
+ Returns:
75
+ WebApiController instance, or None if webapi dependencies not installed.
76
+
77
+ Raises:
78
+ ImportError: If webapi dependencies are not available.
79
+ """
80
+ # pylint: disable=global-statement
81
+ global _CONTROLLER_INSTANCE # noqa: PLW0603
82
+
83
+ if not WEBAPI_AVAILABLE:
84
+ raise ImportError(
85
+ "Web API dependencies not installed. "
86
+ "Install with: pip install datalab-platform[webapi]"
87
+ )
88
+
89
+ if _CONTROLLER_INSTANCE is None:
90
+ # pylint: disable=import-outside-toplevel
91
+ from datalab.webapi.controller import WebApiController
92
+
93
+ _CONTROLLER_INSTANCE = WebApiController()
94
+
95
+ return _CONTROLLER_INSTANCE
@@ -0,0 +1,318 @@
1
+ # Copyright (c) DataLab Platform Developers, BSD 3-Clause License
2
+ # See LICENSE file for details
3
+
4
+ """
5
+ Web API GUI Actions
6
+ ===================
7
+
8
+ GUI actions for controlling the DataLab Web API server.
9
+
10
+ This module provides menu actions and status display for the Web API feature.
11
+ It integrates with the DataLab main window to provide UI controls for:
12
+
13
+ - Starting/stopping the Web API server
14
+ - Viewing connection information (URL, token)
15
+ - Copying connection info to clipboard
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import TYPE_CHECKING
21
+
22
+ from guidata.configtools import get_icon
23
+ from guidata.qthelpers import add_actions, create_action
24
+ from qtpy import QtWidgets as QW
25
+
26
+ from datalab.config import APP_NAME, _
27
+
28
+ if TYPE_CHECKING:
29
+ from datalab.gui.main import DLMainWindow
30
+
31
+
32
+ class WebApiActions:
33
+ """Manager for Web API GUI actions.
34
+
35
+ This class creates and manages the menu actions for the Web API feature.
36
+ It handles the server lifecycle through the WebApiController.
37
+
38
+ Attributes:
39
+ main_window: Reference to the DataLab main window.
40
+ """
41
+
42
+ def __init__(self, main_window: DLMainWindow) -> None:
43
+ """Initialize Web API actions.
44
+
45
+ Args:
46
+ main_window: The DataLab main window.
47
+ """
48
+ self._main_window = main_window
49
+ self._controller = None
50
+ self._menu: QW.QMenu | None = None
51
+ self._start_action: QW.QAction | None = None
52
+ self._stop_action: QW.QAction | None = None
53
+ self._copy_action: QW.QAction | None = None
54
+ self._status_action: QW.QAction | None = None
55
+
56
+ self._init_controller()
57
+ self._create_actions()
58
+
59
+ def _init_controller(self) -> None:
60
+ """Initialize the Web API controller if available."""
61
+ try:
62
+ # pylint: disable=import-outside-toplevel
63
+ from datalab.webapi import WEBAPI_AVAILABLE, get_webapi_controller
64
+
65
+ if WEBAPI_AVAILABLE:
66
+ self._controller = get_webapi_controller()
67
+ self._controller.set_main_window(self._main_window)
68
+ self._controller.server_started.connect(self._on_server_started)
69
+ self._controller.server_stopped.connect(self._on_server_stopped)
70
+ self._controller.server_error.connect(self._on_server_error)
71
+ except ImportError:
72
+ self._controller = None
73
+
74
+ def _create_actions(self) -> None:
75
+ """Create menu actions."""
76
+ available = self._controller is not None
77
+
78
+ # Start action
79
+ self._start_action = create_action(
80
+ self._main_window,
81
+ _("Start Web API Server"),
82
+ icon=get_icon("start_webapi_server.svg"),
83
+ triggered=self._start_server,
84
+ tip=_("Start the HTTP/JSON Web API server for external access"),
85
+ )
86
+ self._start_action.setEnabled(available)
87
+
88
+ # Stop action
89
+ self._stop_action = create_action(
90
+ self._main_window,
91
+ _("Stop Web API Server"),
92
+ icon=get_icon("stop_webapi_server.svg"),
93
+ triggered=self._stop_server,
94
+ )
95
+ self._stop_action.setEnabled(False)
96
+
97
+ # Copy connection info action
98
+ self._copy_action = create_action(
99
+ self._main_window,
100
+ _("Copy Connection Info"),
101
+ icon=get_icon("copy_connection_info.svg"),
102
+ triggered=self._copy_connection_info,
103
+ tip=_("Copy URL and token to clipboard"),
104
+ )
105
+ self._copy_action.setEnabled(False)
106
+
107
+ # Status indicator (not clickable)
108
+ self._status_action = create_action(
109
+ self._main_window,
110
+ _("Status: Not running"),
111
+ )
112
+ self._status_action.setEnabled(False)
113
+
114
+ if not available:
115
+ self._status_action.setText(
116
+ _("Web API unavailable (install datalab-platform[webapi])")
117
+ )
118
+
119
+ def create_menu(self, parent_menu: QW.QMenu) -> QW.QMenu:
120
+ """Create the Web API submenu.
121
+
122
+ Args:
123
+ parent_menu: Parent menu to add submenu to.
124
+
125
+ Returns:
126
+ The created submenu.
127
+ """
128
+ self._menu = parent_menu.addMenu(_("Web API"))
129
+ add_actions(
130
+ self._menu,
131
+ [
132
+ self._start_action,
133
+ self._stop_action,
134
+ None,
135
+ self._copy_action,
136
+ None,
137
+ self._status_action,
138
+ ],
139
+ )
140
+ return self._menu
141
+
142
+ def _start_server(self) -> None:
143
+ """Start the Web API server."""
144
+ if self._controller is None:
145
+ return
146
+
147
+ try:
148
+ url, token = self._controller.start()
149
+ self._show_connection_dialog(url, token)
150
+ except Exception as e: # pylint: disable=broad-exception-caught
151
+ QW.QMessageBox.critical(
152
+ self._main_window,
153
+ APP_NAME,
154
+ _("Failed to start Web API server:") + f"\n{e}",
155
+ )
156
+
157
+ def _stop_server(self) -> None:
158
+ """Stop the Web API server."""
159
+ if self._controller is None:
160
+ return
161
+
162
+ self._controller.stop()
163
+
164
+ def _copy_connection_info(self) -> None:
165
+ """Copy connection info to clipboard."""
166
+ if self._controller is None or not self._controller.is_running:
167
+ return
168
+
169
+ info = self._controller.get_connection_info()
170
+ text = f"URL: {info['url']}\nToken: {info['token']}"
171
+
172
+ clipboard = QW.QApplication.clipboard()
173
+ clipboard.setText(text)
174
+
175
+ # Show brief notification in status bar
176
+ self._main_window.statusBar().showMessage(
177
+ _("Connection info copied to clipboard"), 3000
178
+ )
179
+
180
+ def _show_connection_dialog(self, url: str, token: str) -> None:
181
+ """Show dialog with connection information.
182
+
183
+ Args:
184
+ url: Server URL.
185
+ token: Authentication token.
186
+ """
187
+ dialog = QW.QDialog(self._main_window)
188
+ dialog.setWindowTitle(_("Web API Server Started"))
189
+ dialog.setMinimumWidth(450)
190
+
191
+ layout = QW.QVBoxLayout(dialog)
192
+
193
+ # Info label
194
+ info_label = QW.QLabel(
195
+ _(
196
+ "The Web API server is now running. "
197
+ "Use the following credentials to connect:"
198
+ )
199
+ )
200
+ info_label.setWordWrap(True)
201
+ layout.addWidget(info_label)
202
+
203
+ # URL field
204
+ url_layout = QW.QHBoxLayout()
205
+ url_layout.addWidget(QW.QLabel(_("URL:")))
206
+ url_edit = QW.QLineEdit(url)
207
+ url_edit.setReadOnly(True)
208
+ url_layout.addWidget(url_edit)
209
+ layout.addLayout(url_layout)
210
+
211
+ # Token field
212
+ token_layout = QW.QHBoxLayout()
213
+ token_layout.addWidget(QW.QLabel(_("Token:")))
214
+ token_edit = QW.QLineEdit(token)
215
+ token_edit.setReadOnly(True)
216
+ token_layout.addWidget(token_edit)
217
+ layout.addLayout(token_layout)
218
+
219
+ # Environment variable hint
220
+ hint_label = QW.QLabel(
221
+ _("Tip: Set these environment variables in your notebook:\n")
222
+ + f"DATALAB_WORKSPACE_URL={url}\n"
223
+ + f"DATALAB_WORKSPACE_TOKEN={token}"
224
+ )
225
+ hint_label.setStyleSheet("color: gray; font-size: 10px;")
226
+ layout.addWidget(hint_label)
227
+
228
+ # Buttons
229
+ button_box = QW.QDialogButtonBox()
230
+
231
+ copy_btn = button_box.addButton(
232
+ _("Copy to Clipboard"), QW.QDialogButtonBox.ActionRole
233
+ )
234
+ copy_btn.clicked.connect(self._copy_connection_info)
235
+
236
+ close_btn = button_box.addButton(QW.QDialogButtonBox.Close)
237
+ close_btn.clicked.connect(dialog.accept)
238
+
239
+ layout.addWidget(button_box)
240
+
241
+ dialog.exec_()
242
+
243
+ def _on_server_started(self, url: str, _token: str) -> None:
244
+ """Handle server started signal."""
245
+ self._start_action.setEnabled(False)
246
+ self._stop_action.setEnabled(True)
247
+ self._copy_action.setEnabled(True)
248
+ self._status_action.setText(_("Status: Running at {}").format(url))
249
+ # Update status bar widget
250
+ if self._main_window.webapistatus is not None:
251
+ # Extract port from URL
252
+ try:
253
+ # pylint: disable=import-outside-toplevel
254
+ from urllib.parse import urlparse
255
+
256
+ parsed = urlparse(url)
257
+ port = parsed.port
258
+ except Exception: # pylint: disable=broad-exception-caught
259
+ port = None
260
+ self._main_window.webapistatus.set_status(url, port)
261
+
262
+ def _on_server_stopped(self) -> None:
263
+ """Handle server stopped signal."""
264
+ self._start_action.setEnabled(True)
265
+ self._stop_action.setEnabled(False)
266
+ self._copy_action.setEnabled(False)
267
+ self._status_action.setText(_("Status: Not running"))
268
+ # Update status bar widget
269
+ if self._main_window.webapistatus is not None:
270
+ self._main_window.webapistatus.set_status(None, None)
271
+
272
+ def _on_server_error(self, message: str) -> None:
273
+ """Handle server error signal."""
274
+ QW.QMessageBox.warning(
275
+ self._main_window,
276
+ APP_NAME,
277
+ _("Web API server error:") + f"\n{message}",
278
+ )
279
+ self._on_server_stopped()
280
+
281
+ def cleanup(self) -> None:
282
+ """Clean up resources on shutdown."""
283
+ if self._controller is not None and self._controller.is_running:
284
+ self._controller.stop()
285
+
286
+ def show_connection_info(self) -> None:
287
+ """Show connection info dialog or copy to clipboard.
288
+
289
+ This is called when the status widget is clicked while server is running.
290
+ """
291
+ if self._controller is None or not self._controller.is_running:
292
+ return
293
+
294
+ # Show the connection info dialog
295
+ info = self._controller.get_connection_info()
296
+ self._show_connection_dialog(info["url"], info["token"])
297
+
298
+ def start_server_from_status_widget(self) -> None:
299
+ """Start server from status widget click.
300
+
301
+ This is called when the status widget is clicked while server is not running.
302
+ Shows a confirmation dialog before starting the server.
303
+ """
304
+ # Show confirmation dialog
305
+ answer = QW.QMessageBox.question(
306
+ self._main_window,
307
+ _("Start Web API Server"),
308
+ _(
309
+ "Do you want to start the Web API server?\n\n"
310
+ "This will allow external applications to connect to DataLab "
311
+ "and control it remotely via HTTP/JSON."
312
+ ),
313
+ QW.QMessageBox.Yes | QW.QMessageBox.No,
314
+ QW.QMessageBox.No,
315
+ )
316
+
317
+ if answer == QW.QMessageBox.Yes:
318
+ self._start_server()