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
datalab/gui/main.py
CHANGED
|
@@ -71,6 +71,8 @@ from datalab.utils.qthelpers import (
|
|
|
71
71
|
bring_to_front,
|
|
72
72
|
configure_menu_about_to_show,
|
|
73
73
|
)
|
|
74
|
+
from datalab.webapi import WEBAPI_AVAILABLE, get_webapi_controller
|
|
75
|
+
from datalab.webapi.actions import WebApiActions
|
|
74
76
|
from datalab.widgets import instconfviewer, logviewer, status
|
|
75
77
|
from datalab.widgets.warningerror import go_to_error
|
|
76
78
|
|
|
@@ -146,6 +148,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
146
148
|
self.__old_size: tuple[int, int] | None = None
|
|
147
149
|
self.__memory_warning = False
|
|
148
150
|
self.memorystatus: status.MemoryStatus | None = None
|
|
151
|
+
self.webapistatus: status.WebAPIStatus | None = None
|
|
149
152
|
|
|
150
153
|
self.consolestatus: status.ConsoleStatus | None = None
|
|
151
154
|
self.console: DockableConsole | None = None
|
|
@@ -162,6 +165,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
162
165
|
self.tabmenu: QW.QMenu | None = None
|
|
163
166
|
self.docks: dict[AbstractPanel | DockableConsole, QW.QDockWidget] | None = None
|
|
164
167
|
self.h5inputoutput = H5InputOutput(self)
|
|
168
|
+
self.webapi_actions: WebApiActions | None = None
|
|
165
169
|
|
|
166
170
|
self.openh5_action: QW.QAction | None = None
|
|
167
171
|
self.saveh5_action: QW.QAction | None = None
|
|
@@ -448,6 +452,116 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
448
452
|
panel = self.__get_current_basedatapanel()
|
|
449
453
|
panel.delete_metadata(refresh_plot, keep_roi)
|
|
450
454
|
|
|
455
|
+
@remote_controlled
|
|
456
|
+
def call_method(
|
|
457
|
+
self,
|
|
458
|
+
method_name: str,
|
|
459
|
+
*args,
|
|
460
|
+
panel: Literal["signal", "image"] | None = None,
|
|
461
|
+
**kwargs,
|
|
462
|
+
):
|
|
463
|
+
"""Call a public method on a panel or main window.
|
|
464
|
+
|
|
465
|
+
This generic method allows calling any public method that is not explicitly
|
|
466
|
+
exposed in the proxy API. The method resolution follows this order:
|
|
467
|
+
|
|
468
|
+
1. If panel is specified: call method on that specific panel
|
|
469
|
+
2. If panel is None:
|
|
470
|
+
a. Try to call method on main window (DLMainWindow)
|
|
471
|
+
b. If not found, try to call method on current panel (BaseDataPanel)
|
|
472
|
+
|
|
473
|
+
This makes it convenient to call panel methods without specifying the panel
|
|
474
|
+
parameter when working on the current panel.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
method_name: Name of the method to call
|
|
478
|
+
*args: Positional arguments to pass to the method
|
|
479
|
+
panel: Panel name ("signal", "image", or None for auto-detection).
|
|
480
|
+
Defaults to None.
|
|
481
|
+
**kwargs: Keyword arguments to pass to the method
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
The return value of the called method
|
|
485
|
+
|
|
486
|
+
Raises:
|
|
487
|
+
AttributeError: If the method does not exist or is not public
|
|
488
|
+
ValueError: If the panel name is invalid
|
|
489
|
+
|
|
490
|
+
Examples:
|
|
491
|
+
>>> # Call remove_object on current panel (auto-detected)
|
|
492
|
+
>>> win.call_method("remove_object", force=True)
|
|
493
|
+
>>> # Call a signal panel method specifically
|
|
494
|
+
>>> win.call_method("delete_all_objects", panel="signal")
|
|
495
|
+
>>> # Call main window method
|
|
496
|
+
>>> win.call_method("get_current_panel")
|
|
497
|
+
"""
|
|
498
|
+
# Security check: only allow public methods (not starting with _)
|
|
499
|
+
if method_name.startswith("_"):
|
|
500
|
+
raise AttributeError(
|
|
501
|
+
f"Cannot call private method '{method_name}' through proxy"
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
# If panel is specified, use that panel directly
|
|
505
|
+
if panel is not None:
|
|
506
|
+
target = self.__get_datapanel(panel)
|
|
507
|
+
if not hasattr(target, method_name):
|
|
508
|
+
raise AttributeError(
|
|
509
|
+
f"Method '{method_name}' does not exist on {panel} panel"
|
|
510
|
+
)
|
|
511
|
+
method = getattr(target, method_name)
|
|
512
|
+
if not callable(method):
|
|
513
|
+
raise AttributeError(f"'{method_name}' is not a callable method")
|
|
514
|
+
return method(*args, **kwargs)
|
|
515
|
+
|
|
516
|
+
# Panel is None: try main window first, then current panel
|
|
517
|
+
# Try main window first
|
|
518
|
+
if hasattr(self, method_name):
|
|
519
|
+
method = getattr(self, method_name)
|
|
520
|
+
if callable(method):
|
|
521
|
+
return method(*args, **kwargs)
|
|
522
|
+
|
|
523
|
+
# Method not found on main window, try current panel
|
|
524
|
+
current_panel = self.__get_current_basedatapanel()
|
|
525
|
+
if hasattr(current_panel, method_name):
|
|
526
|
+
method = getattr(current_panel, method_name)
|
|
527
|
+
if callable(method):
|
|
528
|
+
return method(*args, **kwargs)
|
|
529
|
+
|
|
530
|
+
# Method not found anywhere
|
|
531
|
+
raise AttributeError(
|
|
532
|
+
f"Method '{method_name}' does not exist on main window or current panel"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
@remote_controlled
|
|
536
|
+
def call_method_slot(
|
|
537
|
+
self,
|
|
538
|
+
method_name: str,
|
|
539
|
+
args: list,
|
|
540
|
+
panel: Literal["signal", "image"] | None,
|
|
541
|
+
kwargs: dict,
|
|
542
|
+
) -> None:
|
|
543
|
+
"""Slot to call a method from RemoteServer thread in GUI thread.
|
|
544
|
+
|
|
545
|
+
This slot receives signals from RemoteServer and executes the method in
|
|
546
|
+
the GUI thread, avoiding thread-safety issues with Qt widgets and dialogs.
|
|
547
|
+
|
|
548
|
+
Args:
|
|
549
|
+
method_name: Name of the method to call
|
|
550
|
+
args: Positional arguments as a list
|
|
551
|
+
panel: Panel name or None for auto-detection
|
|
552
|
+
kwargs: Keyword arguments as a dict
|
|
553
|
+
"""
|
|
554
|
+
# Call the method and store result in RemoteServer
|
|
555
|
+
try:
|
|
556
|
+
result = self.call_method(method_name, *args, panel=panel, **kwargs)
|
|
557
|
+
# Store result in RemoteServer for retrieval by XML-RPC thread
|
|
558
|
+
self.remote_server.result = result
|
|
559
|
+
self.remote_server.exception = None # Clear any previous exception
|
|
560
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
561
|
+
# Store exception for re-raising in XML-RPC thread
|
|
562
|
+
self.remote_server.result = None
|
|
563
|
+
self.remote_server.exception = exc
|
|
564
|
+
|
|
451
565
|
@remote_controlled
|
|
452
566
|
def get_object_shapes(
|
|
453
567
|
self,
|
|
@@ -526,6 +640,60 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
526
640
|
"""
|
|
527
641
|
self.macropanel.import_macro_from_file(filename)
|
|
528
642
|
|
|
643
|
+
# ------WebAPI control
|
|
644
|
+
@remote_controlled
|
|
645
|
+
def start_webapi_server(
|
|
646
|
+
self,
|
|
647
|
+
host: str | None = None,
|
|
648
|
+
port: int | None = None,
|
|
649
|
+
) -> dict:
|
|
650
|
+
"""Start the Web API server.
|
|
651
|
+
|
|
652
|
+
Args:
|
|
653
|
+
host: Host address to bind to. Defaults to "127.0.0.1".
|
|
654
|
+
port: Port number. Defaults to auto-detect available port.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
Dictionary with "url" and "token" keys.
|
|
658
|
+
|
|
659
|
+
Raises:
|
|
660
|
+
RuntimeError: If Web API deps not installed or server already running.
|
|
661
|
+
"""
|
|
662
|
+
if not WEBAPI_AVAILABLE:
|
|
663
|
+
raise RuntimeError(
|
|
664
|
+
"Web API dependencies not installed. "
|
|
665
|
+
"Install with: pip install datalab-platform[webapi]"
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
controller = get_webapi_controller()
|
|
669
|
+
controller.set_main_window(self)
|
|
670
|
+
url, token = controller.start(host=host, port=port)
|
|
671
|
+
return {"url": url, "token": token}
|
|
672
|
+
|
|
673
|
+
@remote_controlled
|
|
674
|
+
def stop_webapi_server(self) -> None:
|
|
675
|
+
"""Stop the Web API server."""
|
|
676
|
+
if not WEBAPI_AVAILABLE:
|
|
677
|
+
return
|
|
678
|
+
|
|
679
|
+
controller = get_webapi_controller()
|
|
680
|
+
controller.stop()
|
|
681
|
+
|
|
682
|
+
@remote_controlled
|
|
683
|
+
def get_webapi_status(self) -> dict:
|
|
684
|
+
"""Get Web API server status.
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
Dictionary with "running", "url", and "token" keys.
|
|
688
|
+
"""
|
|
689
|
+
if not WEBAPI_AVAILABLE:
|
|
690
|
+
return {"running": False, "url": None, "token": None, "available": False}
|
|
691
|
+
|
|
692
|
+
controller = get_webapi_controller()
|
|
693
|
+
info = controller.get_connection_info()
|
|
694
|
+
info["available"] = True
|
|
695
|
+
return info
|
|
696
|
+
|
|
529
697
|
# ------Misc.
|
|
530
698
|
@property
|
|
531
699
|
def panels(self) -> tuple[AbstractPanel, ...]:
|
|
@@ -540,6 +708,16 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
540
708
|
"""Set memory warning state"""
|
|
541
709
|
self.__memory_warning = state
|
|
542
710
|
|
|
711
|
+
def __show_webapi_info(self) -> None:
|
|
712
|
+
"""Show Web API connection info when status widget is clicked."""
|
|
713
|
+
if self.webapi_actions is not None:
|
|
714
|
+
self.webapi_actions.show_connection_info()
|
|
715
|
+
|
|
716
|
+
def __start_webapi_server(self) -> None:
|
|
717
|
+
"""Start Web API server when status widget is clicked."""
|
|
718
|
+
if self.webapi_actions is not None:
|
|
719
|
+
self.webapi_actions.start_server_from_status_widget()
|
|
720
|
+
|
|
543
721
|
def confirm_memory_state(self) -> bool: # pragma: no cover
|
|
544
722
|
"""Check memory warning state and eventually show a warning dialog
|
|
545
723
|
|
|
@@ -600,6 +778,10 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
600
778
|
# Showing the log viewer for testing purpose (unattended mode) but only
|
|
601
779
|
# if option 'do_not_quit' is not set, to avoid blocking the test suite
|
|
602
780
|
self.__show_logviewer()
|
|
781
|
+
elif execenv.do_not_quit:
|
|
782
|
+
# If 'do_not_quit' is set, we do not show any message box to avoid blocking
|
|
783
|
+
# the test suite
|
|
784
|
+
return
|
|
603
785
|
elif Conf.main.faulthandler_log_available.get(
|
|
604
786
|
False
|
|
605
787
|
) or Conf.main.traceback_log_available.get(False):
|
|
@@ -677,6 +859,12 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
677
859
|
if tour:
|
|
678
860
|
Conf.main.tour_enabled.set(False)
|
|
679
861
|
self.show_tour()
|
|
862
|
+
# Auto-start WebAPI server if environment variable is set
|
|
863
|
+
if os.environ.get("DATALAB_WEBAPI_ENABLED") == "1":
|
|
864
|
+
try:
|
|
865
|
+
self.start_webapi_server()
|
|
866
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
867
|
+
print(f"Warning: Failed to auto-start WebAPI server: {e}")
|
|
680
868
|
|
|
681
869
|
def take_screenshot(self, name: str) -> None: # pragma: no cover
|
|
682
870
|
"""Take main window screenshot"""
|
|
@@ -788,6 +976,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
788
976
|
self.__create_plugins_actions()
|
|
789
977
|
self.__setup_central_widget()
|
|
790
978
|
self.__add_menus()
|
|
979
|
+
self.__setup_webapi()
|
|
791
980
|
if console:
|
|
792
981
|
self.__setup_console()
|
|
793
982
|
self.__update_actions(update_other_data_panel=True)
|
|
@@ -796,6 +985,11 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
796
985
|
# Now that everything is set up, we can restore the window state:
|
|
797
986
|
self.__restore_state()
|
|
798
987
|
|
|
988
|
+
def __setup_webapi(self) -> None:
|
|
989
|
+
"""Setup Web API actions."""
|
|
990
|
+
self.webapi_actions = WebApiActions(self)
|
|
991
|
+
# Note: Menu is added in __update_view_menu since view_menu is cleared each show
|
|
992
|
+
|
|
799
993
|
def __register_plugins(self) -> None:
|
|
800
994
|
"""Register plugins"""
|
|
801
995
|
with qth.try_or_log_error("Discovering plugins"):
|
|
@@ -842,6 +1036,11 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
842
1036
|
xmlrpcstatus = status.XMLRPCStatus()
|
|
843
1037
|
xmlrpcstatus.set_port(self.remote_server.port)
|
|
844
1038
|
self.statusBar().addPermanentWidget(xmlrpcstatus)
|
|
1039
|
+
# Web API server status
|
|
1040
|
+
self.webapistatus = status.WebAPIStatus()
|
|
1041
|
+
self.webapistatus.SIG_SHOW_INFO.connect(self.__show_webapi_info)
|
|
1042
|
+
self.webapistatus.SIG_START_SERVER.connect(self.__start_webapi_server)
|
|
1043
|
+
self.statusBar().addPermanentWidget(self.webapistatus)
|
|
845
1044
|
# Memory status
|
|
846
1045
|
threshold = Conf.main.available_memory_threshold.get()
|
|
847
1046
|
self.memorystatus = status.MemoryStatus(threshold)
|
|
@@ -1304,7 +1503,9 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1304
1503
|
raise ValueError(f"Unknown panel {panel}")
|
|
1305
1504
|
|
|
1306
1505
|
@remote_controlled
|
|
1307
|
-
def calc(
|
|
1506
|
+
def calc(
|
|
1507
|
+
self, name: str, param: gds.DataSet | None = None, edit: bool = True
|
|
1508
|
+
) -> None:
|
|
1308
1509
|
"""Call computation feature ``name``
|
|
1309
1510
|
|
|
1310
1511
|
.. note::
|
|
@@ -1317,6 +1518,8 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1317
1518
|
Args:
|
|
1318
1519
|
name: Compute function name
|
|
1319
1520
|
param: Compute function parameter. Defaults to None.
|
|
1521
|
+
edit: Whether to show parameter edit dialog. Defaults to True.
|
|
1522
|
+
Set to False when calling from remote/API to avoid blocking dialogs.
|
|
1320
1523
|
|
|
1321
1524
|
Raises:
|
|
1322
1525
|
ValueError: unknown function
|
|
@@ -1340,7 +1543,7 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1340
1543
|
# registered feature:
|
|
1341
1544
|
try:
|
|
1342
1545
|
feature = panel.processor.get_feature(name)
|
|
1343
|
-
panel.processor.run_feature(feature, param)
|
|
1546
|
+
panel.processor.run_feature(feature, param, edit=edit)
|
|
1344
1547
|
return
|
|
1345
1548
|
except ValueError:
|
|
1346
1549
|
continue
|
|
@@ -1438,6 +1641,10 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1438
1641
|
self.settings_action,
|
|
1439
1642
|
],
|
|
1440
1643
|
)
|
|
1644
|
+
# Add Web API submenu
|
|
1645
|
+
if self.webapi_actions is not None:
|
|
1646
|
+
self.file_menu.addSeparator()
|
|
1647
|
+
self.webapi_actions.create_menu(self.file_menu)
|
|
1441
1648
|
if self.quit_action is not None:
|
|
1442
1649
|
add_actions(self.file_menu, [self.quit_action])
|
|
1443
1650
|
|
|
@@ -1531,6 +1738,16 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
1531
1738
|
if panel is not None:
|
|
1532
1739
|
panel.remove_all_objects()
|
|
1533
1740
|
|
|
1741
|
+
@remote_controlled
|
|
1742
|
+
def remove_object(self, force: bool = False) -> None:
|
|
1743
|
+
"""Remove current object from current panel.
|
|
1744
|
+
|
|
1745
|
+
Args:
|
|
1746
|
+
force: if True, remove object without confirmation. Defaults to False.
|
|
1747
|
+
"""
|
|
1748
|
+
panel = self.__get_current_basedatapanel()
|
|
1749
|
+
panel.remove_object(force)
|
|
1750
|
+
|
|
1534
1751
|
@staticmethod
|
|
1535
1752
|
def __check_h5file(filename: str, operation: str) -> str:
|
|
1536
1753
|
"""Check HDF5 filename"""
|
|
@@ -2115,6 +2332,8 @@ class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta
|
|
|
2115
2332
|
# it would represent too much effort for an error occuring in test
|
|
2116
2333
|
# configurations only.
|
|
2117
2334
|
pass
|
|
2335
|
+
if self.webapi_actions is not None:
|
|
2336
|
+
self.webapi_actions.cleanup()
|
|
2118
2337
|
self.reset_all()
|
|
2119
2338
|
self.__save_pos_size_and_state()
|
|
2120
2339
|
self.__unregister_plugins()
|
datalab/gui/settings.py
CHANGED
|
@@ -47,6 +47,16 @@ class MainSettings(gds.DataSet):
|
|
|
47
47
|
"<br>like your own scripts (e.g. from Spyder or Jupyter) or other software."
|
|
48
48
|
),
|
|
49
49
|
)
|
|
50
|
+
webapi_localhost_no_token = gds.BoolItem(
|
|
51
|
+
"",
|
|
52
|
+
_("Web API localhost bypass"),
|
|
53
|
+
help=_(
|
|
54
|
+
"When enabled (default), connections from localhost (127.0.0.1) to the "
|
|
55
|
+
"<br>Web API do not require authentication. This simplifies notebook "
|
|
56
|
+
"<br>integration when DataLab-Kernel runs on the same machine."
|
|
57
|
+
"<br>Disable for stricter security if needed."
|
|
58
|
+
),
|
|
59
|
+
)
|
|
50
60
|
available_memory_threshold = gds.IntItem(
|
|
51
61
|
_("Memory threshold"),
|
|
52
62
|
default=0,
|
datalab/gui/tour.py
CHANGED
|
@@ -456,9 +456,8 @@ class BaseTour(QW.QWidget, metaclass=BaseTourMeta):
|
|
|
456
456
|
Args:
|
|
457
457
|
factor: Factor by which the size of the window is multiplied.
|
|
458
458
|
"""
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
screen_geometry = desktop.screenGeometry(screen)
|
|
459
|
+
screen = QW.QApplication.primaryScreen()
|
|
460
|
+
screen_geometry = screen.geometry()
|
|
462
461
|
width = int(screen_geometry.width() * factor)
|
|
463
462
|
height = int(screen_geometry.height() * factor)
|
|
464
463
|
self.win.resize(width, height)
|
|
Binary file
|
|
@@ -1298,7 +1298,7 @@ msgid "Macro panel"
|
|
|
1298
1298
|
msgstr "Panneau des macros"
|
|
1299
1299
|
|
|
1300
1300
|
msgid "Python files"
|
|
1301
|
-
msgstr "Fichiers Python"
|
|
1301
|
+
msgstr "Fichiers Python"
|
|
1302
1302
|
|
|
1303
1303
|
msgid "-***- Macro Console -***-"
|
|
1304
1304
|
msgstr "-***- Console des macros -***-"
|
|
@@ -2162,6 +2162,12 @@ msgstr "Serveur XML-RPC"
|
|
|
2162
2162
|
msgid "RPC server is used to communicate with external applications,<br>like your own scripts (e.g. from Spyder or Jupyter) or other software."
|
|
2163
2163
|
msgstr "Le serveur XML-RPC est utilisé pour communiquer avec des applications externes,<br>comme vos propres scripts (par exemple depuis Spyder ou Jupyter) ou d'autres logiciels."
|
|
2164
2164
|
|
|
2165
|
+
msgid "Web API localhost bypass"
|
|
2166
|
+
msgstr "Contournement localhost de l'API Web"
|
|
2167
|
+
|
|
2168
|
+
msgid "When enabled (default), connections from localhost (127.0.0.1) to the <br>Web API do not require authentication. This simplifies notebook <br>integration when DataLab-Kernel runs on the same machine.<br>Disable for stricter security if needed."
|
|
2169
|
+
msgstr "Lorsque cette option est activée (par défaut), les connexions depuis localhost (127.0.0.1) <br>vers l'API Web ne nécessitent pas d'authentification. Cela simplifie l'intégration <br>des notebooks lorsque DataLab-Kernel s'exécute sur la même machine.<br>Désactiver pour une sécurité plus stricte si nécessaire."
|
|
2170
|
+
|
|
2165
2171
|
msgid "Memory threshold"
|
|
2166
2172
|
msgstr "Seuil de mémoire"
|
|
2167
2173
|
|
|
@@ -2804,6 +2810,70 @@ msgstr "dans ce dossier"
|
|
|
2804
2810
|
msgid "Open tab menu"
|
|
2805
2811
|
msgstr "Ouvrir le menu des onglets"
|
|
2806
2812
|
|
|
2813
|
+
msgid "Start Web API Server"
|
|
2814
|
+
msgstr "Démarrer le serveur API Web"
|
|
2815
|
+
|
|
2816
|
+
msgid "Start the HTTP/JSON Web API server for external access"
|
|
2817
|
+
msgstr "Démarrer le serveur API Web HTTP/JSON pour l'accès externe"
|
|
2818
|
+
|
|
2819
|
+
msgid "Stop Web API Server"
|
|
2820
|
+
msgstr "Arrêter le serveur API Web"
|
|
2821
|
+
|
|
2822
|
+
msgid "Copy Connection Info"
|
|
2823
|
+
msgstr "Copier les infos de connexion"
|
|
2824
|
+
|
|
2825
|
+
msgid "Copy URL and token to clipboard"
|
|
2826
|
+
msgstr "Copier l'URL et le jeton dans le presse-papier"
|
|
2827
|
+
|
|
2828
|
+
msgid "Status: Not running"
|
|
2829
|
+
msgstr "Statut : Non démarré"
|
|
2830
|
+
|
|
2831
|
+
msgid "Web API unavailable (install datalab-platform[webapi])"
|
|
2832
|
+
msgstr "API Web non disponible (installer datalab-platform[webapi])"
|
|
2833
|
+
|
|
2834
|
+
msgid "Web API"
|
|
2835
|
+
msgstr "API Web"
|
|
2836
|
+
|
|
2837
|
+
msgid "Failed to start Web API server:"
|
|
2838
|
+
msgstr "Échec du démarrage du serveur API Web :"
|
|
2839
|
+
|
|
2840
|
+
msgid "Connection info copied to clipboard"
|
|
2841
|
+
msgstr "Infos de connexion copiées dans le presse-papier"
|
|
2842
|
+
|
|
2843
|
+
msgid "Web API Server Started"
|
|
2844
|
+
msgstr "Serveur API Web démarré"
|
|
2845
|
+
|
|
2846
|
+
msgid "The Web API server is now running. Use the following credentials to connect:"
|
|
2847
|
+
msgstr "Le serveur API Web est maintenant en cours d'exécution. Utilisez les identifiants suivants pour vous connecter :"
|
|
2848
|
+
|
|
2849
|
+
msgid "URL:"
|
|
2850
|
+
msgstr "URL :"
|
|
2851
|
+
|
|
2852
|
+
msgid "Token:"
|
|
2853
|
+
msgstr "Jeton :"
|
|
2854
|
+
|
|
2855
|
+
msgid "Tip: Set these environment variables in your notebook:\n"
|
|
2856
|
+
msgstr "Astuce : Définissez ces variables d'environnement dans votre notebook :\n"
|
|
2857
|
+
|
|
2858
|
+
msgid "Copy to Clipboard"
|
|
2859
|
+
msgstr "Copier dans le presse-papier"
|
|
2860
|
+
|
|
2861
|
+
#, python-brace-format
|
|
2862
|
+
msgid "Status: Running at {}"
|
|
2863
|
+
msgstr "Statut : En cours d'exécution à {}"
|
|
2864
|
+
|
|
2865
|
+
msgid "Web API server error:"
|
|
2866
|
+
msgstr "Erreur du serveur API Web :"
|
|
2867
|
+
|
|
2868
|
+
msgid ""
|
|
2869
|
+
"Do you want to start the Web API server?\n"
|
|
2870
|
+
"\n"
|
|
2871
|
+
"This will allow external applications to connect to DataLab and control it remotely via HTTP/JSON."
|
|
2872
|
+
msgstr ""
|
|
2873
|
+
"Souhaitez-vous démarrer le serveur API Web ?\n"
|
|
2874
|
+
"\n"
|
|
2875
|
+
"Cela permettra aux applications externes de se connecter à DataLab et de le contrôler à distance via HTTP/JSON."
|
|
2876
|
+
|
|
2807
2877
|
msgid "Connection to DataLab"
|
|
2808
2878
|
msgstr "Connexion à DataLab"
|
|
2809
2879
|
|
|
@@ -3044,6 +3114,21 @@ msgstr "Plugins :"
|
|
|
3044
3114
|
msgid "XML-RPC:"
|
|
3045
3115
|
msgstr "XML-RPC :"
|
|
3046
3116
|
|
|
3117
|
+
msgid "Web API server is not running"
|
|
3118
|
+
msgstr "Serveur API Web arrêté"
|
|
3119
|
+
|
|
3120
|
+
msgid "Click to start"
|
|
3121
|
+
msgstr "Cliquer pour démarrer"
|
|
3122
|
+
|
|
3123
|
+
msgid "Web API:"
|
|
3124
|
+
msgstr "API Web :"
|
|
3125
|
+
|
|
3126
|
+
msgid "Web API server is running"
|
|
3127
|
+
msgstr "Serveur API Web démarré"
|
|
3128
|
+
|
|
3129
|
+
msgid "Click to view connection info"
|
|
3130
|
+
msgstr "Cliquer pour voir les infos de connexion"
|
|
3131
|
+
|
|
3047
3132
|
msgid "Source"
|
|
3048
3133
|
msgstr "Source"
|
|
3049
3134
|
|
|
@@ -3294,3 +3379,4 @@ msgstr "Merci de sélectionner le fichier à importer."
|
|
|
3294
3379
|
|
|
3295
3380
|
msgid "Example Wizard"
|
|
3296
3381
|
msgstr "Assistant exemple"
|
|
3382
|
+
|
datalab/tests/__init__.py
CHANGED
|
@@ -34,7 +34,7 @@ from sigima.tests import helpers
|
|
|
34
34
|
|
|
35
35
|
import datalab.config # Loading icons
|
|
36
36
|
from datalab.config import MOD_NAME, SHOTPATH
|
|
37
|
-
from datalab.control.proxy import RemoteProxy
|
|
37
|
+
from datalab.control.proxy import RemoteProxy, proxy_context
|
|
38
38
|
from datalab.env import execenv
|
|
39
39
|
from datalab.gui.main import DLMainWindow
|
|
40
40
|
from datalab.gui.panel.image import ImagePanel
|
|
@@ -172,6 +172,37 @@ def run_datalab_in_background(wait_until_ready: bool = True) -> None:
|
|
|
172
172
|
) from exc
|
|
173
173
|
|
|
174
174
|
|
|
175
|
+
def close_datalab_background() -> None:
|
|
176
|
+
"""Close DataLab application running as a service.
|
|
177
|
+
|
|
178
|
+
This function connects to the DataLab application running in the background
|
|
179
|
+
(started with `run_datalab_in_background`) and sends a command to close it.
|
|
180
|
+
It uses the `RemoteProxy` class to establish the connection and send the
|
|
181
|
+
close command.
|
|
182
|
+
|
|
183
|
+
Raises:
|
|
184
|
+
ConnectionRefusedError: If unable to connect to the DataLab application.
|
|
185
|
+
"""
|
|
186
|
+
proxy = RemoteProxy(autoconnect=False)
|
|
187
|
+
proxy.connect(timeout=5.0) # 5 seconds max to connect
|
|
188
|
+
proxy.close_application()
|
|
189
|
+
proxy.disconnect()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@contextmanager
|
|
193
|
+
def datalab_in_background_context() -> Generator[RemoteProxy, None, None]:
|
|
194
|
+
"""Context manager for DataLab instance with proxy connection"""
|
|
195
|
+
run_datalab_in_background()
|
|
196
|
+
with proxy_context("remote") as proxy:
|
|
197
|
+
try:
|
|
198
|
+
yield proxy
|
|
199
|
+
except Exception as exc: # pylint: disable=broad-except
|
|
200
|
+
proxy.close_application()
|
|
201
|
+
raise exc
|
|
202
|
+
# Cleanup
|
|
203
|
+
proxy.close_application()
|
|
204
|
+
|
|
205
|
+
|
|
175
206
|
@contextmanager
|
|
176
207
|
def skip_if_opencv_missing() -> Generator[None, None, None]:
|
|
177
208
|
"""Skip test if OpenCV is not available"""
|
|
@@ -128,7 +128,7 @@ def check_conf(conf, name, win: QW.QMainWindow, h5files):
|
|
|
128
128
|
# Check position, taking into account screen offset (e.g. Linux/Gnome)
|
|
129
129
|
conf_x, conf_y = sec_main[OPT_POS.option]
|
|
130
130
|
conf_w, conf_h = sec_main[OPT_SIZ.option]
|
|
131
|
-
available_go = QW.
|
|
131
|
+
available_go = QW.QApplication.primaryScreen().availableGeometry()
|
|
132
132
|
x_offset, y_offset = available_go.x(), available_go.y()
|
|
133
133
|
assert_in_interval(win.x(), conf_x - x_offset, 0, "X position")
|
|
134
134
|
assert_in_interval(win.y(), conf_y - y_offset / 2, 15 + y_offset, "Y position")
|
|
@@ -54,6 +54,10 @@ def test_main_app():
|
|
|
54
54
|
# Add signals to signal panel
|
|
55
55
|
sig1 = create_paracetamol_signal(500)
|
|
56
56
|
panel.add_object(sig1)
|
|
57
|
+
nobj0 = len(panel)
|
|
58
|
+
win.remove_object(force=True) # Test remove object method, then add it back
|
|
59
|
+
assert len(panel) == nobj0 - 1, "One object should have been removed"
|
|
60
|
+
panel.add_object(sig1)
|
|
57
61
|
panel.rename_selected_object_or_group("Paracetamol Signal 1")
|
|
58
62
|
panel.processor.run_feature(sips.derivative)
|
|
59
63
|
panel.processor.run_feature(sips.wiener)
|
|
@@ -13,7 +13,7 @@ import os
|
|
|
13
13
|
import numpy as np
|
|
14
14
|
import psutil
|
|
15
15
|
from guidata.qthelpers import qt_app_context
|
|
16
|
-
from sigima.
|
|
16
|
+
from sigima.viz import view_curves
|
|
17
17
|
|
|
18
18
|
from datalab.env import execenv
|
|
19
19
|
from datalab.tests.features.control.embedded1_unit_test import HostWindow
|
|
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|
|
14
14
|
|
|
15
15
|
import sigima.objects
|
|
16
16
|
from guidata.qthelpers import qt_app_context
|
|
17
|
-
from sigima.
|
|
17
|
+
from sigima.viz import view_curves, view_images
|
|
18
18
|
|
|
19
19
|
from datalab.env import execenv
|
|
20
20
|
from datalab.gui.newobject import create_image_gui, create_signal_gui
|
|
@@ -164,6 +164,11 @@ def __misc_unit_function(win: DLMainWindow) -> None:
|
|
|
164
164
|
group_id=get_uuid(gp2),
|
|
165
165
|
set_current=False,
|
|
166
166
|
)
|
|
167
|
+
sig = win.get_object() # Get the last added signal
|
|
168
|
+
nobj0 = len(win.signalpanel)
|
|
169
|
+
win.remove_object(force=True) # Test the remove object method, then add it back
|
|
170
|
+
assert len(win.signalpanel) == nobj0 - 1, "One object should have been removed"
|
|
171
|
+
win.add_object(sig)
|
|
167
172
|
|
|
168
173
|
# Add image
|
|
169
174
|
__print_test_result("Add image")
|