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.
- datalab/__init__.py +1 -1
- datalab/config.py +4 -0
- datalab/control/baseproxy.py +160 -0
- datalab/control/remote.py +175 -1
- datalab/data/doc/DataLab_en.pdf +0 -0
- datalab/data/doc/DataLab_fr.pdf +0 -0
- datalab/data/icons/control/copy_connection_info.svg +11 -0
- datalab/data/icons/control/start_webapi_server.svg +19 -0
- datalab/data/icons/control/stop_webapi_server.svg +7 -0
- datalab/gui/main.py +221 -2
- datalab/gui/settings.py +10 -0
- datalab/gui/tour.py +2 -3
- datalab/locale/fr/LC_MESSAGES/datalab.mo +0 -0
- datalab/locale/fr/LC_MESSAGES/datalab.po +87 -1
- datalab/tests/__init__.py +32 -1
- datalab/tests/backbone/config_unit_test.py +1 -1
- datalab/tests/backbone/main_app_test.py +4 -0
- datalab/tests/backbone/memory_leak.py +1 -1
- datalab/tests/features/common/createobject_unit_test.py +1 -1
- datalab/tests/features/common/misc_app_test.py +5 -0
- datalab/tests/features/control/call_method_unit_test.py +104 -0
- datalab/tests/features/control/embedded1_unit_test.py +8 -0
- datalab/tests/features/control/remoteclient_app_test.py +39 -35
- datalab/tests/features/control/simpleclient_unit_test.py +7 -3
- datalab/tests/features/hdf5/h5browser2_unit.py +1 -1
- datalab/tests/features/image/background_dialog_test.py +2 -2
- datalab/tests/features/image/imagetools_unit_test.py +1 -1
- datalab/tests/features/signal/baseline_dialog_test.py +1 -1
- datalab/tests/webapi_test.py +395 -0
- datalab/webapi/__init__.py +95 -0
- datalab/webapi/actions.py +318 -0
- datalab/webapi/adapter.py +642 -0
- datalab/webapi/controller.py +379 -0
- datalab/webapi/routes.py +576 -0
- datalab/webapi/schema.py +198 -0
- datalab/webapi/serialization.py +388 -0
- datalab/widgets/status.py +61 -0
- {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/METADATA +6 -2
- {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/RECORD +45 -33
- /datalab/data/icons/{libre-gui-link.svg → control/libre-gui-link.svg} +0 -0
- /datalab/data/icons/{libre-gui-unlink.svg → control/libre-gui-unlink.svg} +0 -0
- {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/WHEEL +0 -0
- {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/entry_points.txt +0 -0
- {datalab_platform-1.0.4.dist-info → datalab_platform-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {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()
|