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
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(self, name: str, param: gds.DataSet | None = None) -> None:
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
- desktop = QW.QApplication.desktop()
460
- screen = desktop.screenNumber(desktop.cursor().pos())
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.QDesktopWidget().availableGeometry()
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.tests.vistools import view_curves
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.tests.vistools import view_curves, view_images
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")