Source code for datalab.control.remote

# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.

"""
DataLab remote control
----------------------

This module provides utilities to control DataLab from a Python script (e.g. with
Spyder) or from a Jupyter notebook.

The :class:`RemoteClient` class provides the main interface to DataLab XML-RPC server.
"""

from __future__ import annotations

import functools
import sys
import threading
import time
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING
from xmlrpc.client import Binary, ServerProxy
from xmlrpc.server import SimpleXMLRPCServer

import guidata.dataset as gds
import numpy as np
from packaging.version import Version
from qtpy import QtCore as QC
from sigima.client import utils
from sigima.objects import ImageObj, SignalObj, create_image, create_signal

import datalab
from datalab.adapters_plotpy import items_to_json, json_to_items
from datalab.config import Conf, initialize
from datalab.control.baseproxy import AbstractDLControl, BaseProxy
from datalab.env import execenv

if TYPE_CHECKING:
    from datalab.gui.main import DLMainWindow

# pylint: disable=invalid-name  # Allows short reference names like x, y, ...
# pylint: disable=duplicate-code


def remote_call(func: Callable) -> object:
    """Decorator for method calling DataLab main window remotely"""

    @functools.wraps(func)
    def method_wrapper(*args, **kwargs):
        """Decorator wrapper function"""
        self = args[0]  # extracting 'self' from method arguments
        self.is_ready = False
        output = func(*args, **kwargs)
        while not self.is_ready:
            QC.QCoreApplication.processEvents()
            time.sleep(0.05)
        return output

    return method_wrapper


# Note: RemoteServer can't inherit from AbstractDLControl because it is a QThread
# and most of the methods are not returning expected data types


class RemoteServer(QC.QThread):
    """XML-RPC server QThread"""

    SIG_SERVER_PORT = QC.Signal(int)
    SIG_CLOSE_APP = QC.Signal()
    SIG_RAISE_WINDOW = QC.Signal()
    SIG_ADD_OBJECT = QC.Signal(object, str, bool)
    SIG_ADD_GROUP = QC.Signal(str, str, bool)
    SIG_LOAD_FROM_FILES = QC.Signal(list)
    SIG_LOAD_FROM_DIRECTORY = QC.Signal(str)
    SIG_SELECT_OBJECTS = QC.Signal(list, str)
    SIG_SELECT_GROUPS = QC.Signal(list, str)
    SIG_SELECT_ALL_GROUPS = QC.Signal(str)
    SIG_DELETE_METADATA = QC.Signal(bool, bool)
    SIG_SWITCH_TO_PANEL = QC.Signal(str)
    SIG_TOGGLE_AUTO_REFRESH = QC.Signal(bool)
    SIG_TOGGLE_SHOW_TITLES = QC.Signal(bool)
    SIG_RESET_ALL = QC.Signal()
    SIG_SAVE_TO_H5 = QC.Signal(str)
    SIG_OPEN_H5 = QC.Signal(list, bool, bool)
    SIG_IMPORT_H5 = QC.Signal(str, bool)
    SIG_CALC = QC.Signal(str, object)
    SIG_RUN_MACRO = QC.Signal(str)
    SIG_STOP_MACRO = QC.Signal(str)
    SIG_IMPORT_MACRO_FROM_FILE = QC.Signal(str)

    def __init__(self, win: DLMainWindow) -> None:
        QC.QThread.__init__(self)
        self.port: int = None
        self.is_ready = True
        self.server: SimpleXMLRPCServer | None = None
        self.win = win
        win.SIG_READY.connect(self.datalab_is_ready)
        win.SIG_CLOSING.connect(self.shutdown_server)
        self.SIG_CLOSE_APP.connect(win.close)
        self.SIG_RAISE_WINDOW.connect(win.raise_window)
        self.SIG_ADD_OBJECT.connect(win.add_object)
        self.SIG_ADD_GROUP.connect(win.add_group)
        self.SIG_LOAD_FROM_FILES.connect(win.load_from_files)
        self.SIG_LOAD_FROM_DIRECTORY.connect(win.load_from_directory)
        self.SIG_SELECT_OBJECTS.connect(win.select_objects)
        self.SIG_SELECT_GROUPS.connect(win.select_groups)
        self.SIG_SELECT_ALL_GROUPS.connect(lambda panel: win.select_groups(None, panel))
        self.SIG_DELETE_METADATA.connect(win.delete_metadata)
        self.SIG_SWITCH_TO_PANEL.connect(win.set_current_panel)
        self.SIG_TOGGLE_AUTO_REFRESH.connect(win.toggle_auto_refresh)
        self.SIG_TOGGLE_SHOW_TITLES.connect(win.toggle_show_titles)
        self.SIG_RESET_ALL.connect(win.reset_all)
        self.SIG_SAVE_TO_H5.connect(win.save_to_h5_file)
        self.SIG_OPEN_H5.connect(win.open_h5_files)
        self.SIG_IMPORT_H5.connect(win.import_h5_file)
        self.SIG_CALC.connect(win.calc)
        self.SIG_RUN_MACRO.connect(win.run_macro)
        self.SIG_STOP_MACRO.connect(win.stop_macro)
        self.SIG_IMPORT_MACRO_FROM_FILE.connect(win.import_macro_from_file)

    def serve(self) -> None:
        """Start server and serve forever"""
        with SimpleXMLRPCServer(
            ("127.0.0.1", 0), logRequests=False, allow_none=True
        ) as server:
            self.server = server
            server.register_introspection_functions()
            self.register_functions(server)
            self.port = server.server_address[1]
            self.notify_port(self.port)
            with execenv.context(xmlrpcport=self.port):
                server.serve_forever()

    def shutdown_server(self) -> None:
        """Shutdown server"""
        if self.server is not None:
            self.server.shutdown()
            self.server = None

    def notify_port(self, port: int) -> None:
        """Notify automatically attributed port.

        This method is called after the server port has been automatically
        attributed. It notifies the port number to the main window.

        Args:
            port: Server port number
        """
        self.SIG_SERVER_PORT.emit(port)

    @classmethod
    def check_remote_functions(cls) -> None:
        """Check if all AbstractDLControl methods are implemented in RemoteServer"""
        mlist = []
        for method in AbstractDLControl.get_public_methods():
            if not hasattr(cls, method):
                mlist.append(method)
        if mlist:
            raise RuntimeError(f"{cls} is missing some methods: {','.join(mlist)}")

    def register_functions(self, server: SimpleXMLRPCServer) -> None:
        """Register functions"""
        for name in AbstractDLControl.get_public_methods():
            server.register_function(getattr(self, name))

    def run(self) -> None:
        """Thread execution method"""
        if "coverage" in sys.modules:
            # The following is required to make coverage work with threading
            # pylint: disable=protected-access
            sys.settrace(threading._trace_hook)
        self.serve()

    def datalab_is_ready(self) -> None:
        """Called when DataLab is ready to process new requests"""
        self.is_ready = True

    @staticmethod
    def get_version() -> str:
        """Return DataLab public version"""
        return datalab.__version__

    def close_application(self) -> None:
        """Close DataLab application"""
        self.SIG_CLOSE_APP.emit()

    def raise_window(self) -> None:
        """Raise DataLab window"""
        self.SIG_RAISE_WINDOW.emit()

    @remote_call
    def get_current_panel(self) -> str:
        """Return current panel name.

        Returns:
            Panel name (valid values: 'signal', 'image', 'macro')
        """
        return self.win.get_current_panel()

    @remote_call
    def set_current_panel(self, panel: str) -> None:
        """Switch to panel.

        Args:
            panel: Panel name (valid values: 'signal', 'image', 'macro')
        """
        self.SIG_SWITCH_TO_PANEL.emit(panel)

    @remote_call
    def toggle_auto_refresh(self, state: bool) -> None:
        """Toggle auto refresh state.

        Args:
            state: True to enable auto refresh, False to disable it
        """
        self.SIG_TOGGLE_AUTO_REFRESH.emit(state)

    @remote_call
    def toggle_show_titles(self, state: bool) -> None:
        """Toggle show titles state.

        Args:
            state: True to enable show titles, False to disable it
        """
        self.SIG_TOGGLE_SHOW_TITLES.emit(state)

    @remote_call
    def reset_all(self) -> None:
        """Reset all application data"""
        self.SIG_RESET_ALL.emit()

    @remote_call
    def save_to_h5_file(self, filename: str) -> None:
        """Save to a DataLab HDF5 file.

        Args:
            filename: HDF5 file name (with extension .h5)
        """
        self.SIG_SAVE_TO_H5.emit(filename)

    @remote_call
    def open_h5_files(
        self,
        h5files: list[str] | None = None,
        import_all: bool | None = None,
        reset_all: bool | None = None,
    ) -> None:
        """Open a DataLab HDF5 file or import from any other HDF5 file.

        Args:
            h5files: HDF5 file names. Defaults to None.
            import_all: Import all objects from HDF5 file. Defaults to None.
            reset_all: Reset all application data. Defaults to None.
        """
        import_all = True if import_all is None else import_all
        reset_all = False if reset_all is None else reset_all
        self.SIG_OPEN_H5.emit(h5files, import_all, reset_all)

    @remote_call
    def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
        """Open DataLab HDF5 browser to Import HDF5 file.

        Args:
            filename: HDF5 file name
            reset_all: Reset all application data. Defaults to None.
        """
        reset_all = False if reset_all is None else reset_all
        self.SIG_IMPORT_H5.emit(filename, reset_all)

    @remote_call
    def load_from_files(self, filenames: list[str]) -> None:
        """Open objects from files in current panel (signals/images).

        Args:
            filenames: list of file names
        """
        self.SIG_LOAD_FROM_FILES.emit(filenames)

    @remote_call
    def load_from_directory(self, path: str) -> None:
        """Open objects from directory in current panel (signals/images).

        Args:
            path: directory path
        """
        self.SIG_LOAD_FROM_DIRECTORY.emit(path)

    @remote_call
    def add_signal(
        self,
        title: str,
        xbinary: Binary,
        ybinary: Binary,
        xunit: str = "",
        yunit: str = "",
        xlabel: str = "",
        ylabel: str = "",
        group_id: str = "",
        set_current: bool = True,
    ) -> bool:  # pylint: disable=too-many-arguments
        """Add signal data to DataLab.

        Args:
            title: Signal title
            xbinary: X data
            ybinary: Y data
            xunit: X unit. Defaults to ""
            yunit: Y unit. Defaults to ""
            xlabel: X label. Defaults to ""
            ylabel: Y label. Defaults to ""
            group_id: group id in which to add the signal. Defaults to ""
            set_current: if True, set the added signal as current

        Returns:
            True if successful
        """
        xdata = utils.rpcbinary_to_array(xbinary)
        ydata = utils.rpcbinary_to_array(ybinary)
        signal = create_signal(title, xdata, ydata)
        signal.xunit = xunit or ""  # In case xunit is None
        signal.yunit = yunit or ""  # In case yunit is None
        signal.xlabel = xlabel or ""  # In case xlabel is None
        signal.ylabel = ylabel or ""  # In case ylabel is None
        self.SIG_ADD_OBJECT.emit(signal, group_id, set_current)
        return True

    @remote_call
    def add_image(
        self,
        title: str,
        zbinary: Binary,
        xunit: str = "",
        yunit: str = "",
        zunit: str = "",
        xlabel: str = "",
        ylabel: str = "",
        zlabel: str = "",
        group_id: str = "",
        set_current: bool = True,
    ) -> bool:  # pylint: disable=too-many-arguments
        """Add image data to DataLab.

        Args:
            title: Image title
            zbinary: Z data
            xunit: X unit. Defaults to ""
            yunit: Y unit. Defaults to ""
            zunit: Z unit. Defaults to ""
            xlabel: X label. Defaults to ""
            ylabel: Y label. Defaults to ""
            zlabel: Z label. Defaults to ""
            group_id: group id in which to add the image. Defaults to ""
            set_current: if True, set the added image as current

        Returns:
            True if successful
        """
        data = utils.rpcbinary_to_array(zbinary)
        image = create_image(title, data)
        image.xunit = xunit or ""  # In case xunit is None
        image.yunit = yunit or ""  # In case yunit is None
        image.zunit = zunit or ""  # In case zunit is None
        image.xlabel = xlabel or ""  # In case xlabel is None
        image.ylabel = ylabel or ""  # In case ylabel is None
        image.zlabel = zlabel or ""  # In case zlabel is None
        self.SIG_ADD_OBJECT.emit(image, group_id, set_current)
        return True

    @remote_call
    def add_object(
        self, obj_data: list[str], group_id: str = "", set_current: bool = True
    ) -> bool:
        """Add object to DataLab.

        Args:
            obj_data: Object data
            group_id: Group id in which to add the object. Defaults to ""
            set_current: if True, set the added object as current

        Returns:
            True if successful
        """
        obj = utils.rpcjson_to_dataset(obj_data)
        self.SIG_ADD_OBJECT.emit(obj, group_id, set_current)
        return True

    @remote_call
    def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
        """Return selected objects uuids.

        Args:
            include_groups: If True, also return objects from selected groups.

        Returns:
            List of selected objects uuids.
        """
        return self.win.get_sel_object_uuids(include_groups)

    @remote_call
    def add_group(
        self, title: str, panel: str | None = None, select: bool = False
    ) -> None:
        """Add group to DataLab.

        Args:
            title: Group title
            panel: Panel name (valid values: "signal", "image"). Defaults to None.
            select: Select the group after creation. Defaults to False.
        """
        self.SIG_ADD_GROUP.emit(title, panel, select)

    @remote_call
    def select_objects(
        self,
        selection: list[int | str],
        panel: str | None = None,
    ) -> None:
        """Select objects in current panel.

        Args:
            selection: List of object numbers (1 to N) or uuids to select
            panel: panel name (valid values: "signal", "image").
             If None, current panel is used. Defaults to None.
        """
        self.SIG_SELECT_OBJECTS.emit(selection, panel)

    @remote_call
    def select_groups(
        self, selection: list[int | str] | None = None, panel: str | None = None
    ) -> None:
        """Select groups in current panel.

        Args:
            selection: List of group numbers (1 to N), or list of group uuids,
             or None to select all groups. Defaults to None.
            panel: panel name (valid values: "signal", "image").
             If None, current panel is used. Defaults to None.
        """
        if selection is None:
            self.SIG_SELECT_ALL_GROUPS.emit(panel)
        else:
            self.SIG_SELECT_GROUPS.emit(selection, panel)

    @remote_call
    def delete_metadata(
        self, refresh_plot: bool = True, keep_roi: bool = False
    ) -> None:
        """Delete metadata of selected objects

        Args:
            refresh_plot: Refresh plot. Defaults to True.
            keep_roi: Keep ROI. Defaults to False.
        """
        self.SIG_DELETE_METADATA.emit(refresh_plot, keep_roi)

    @remote_call
    def calc(self, name: str, param_data: list[str] | None = None) -> bool:
        """Call computation feature ``name``

        .. note::

            This calls either the processor's ``compute_<name>`` method (if it exists),
            or the processor's ``<name>`` computation feature (if it is registered,
            using the ``run_feature`` method).
            It looks for the function in all panels, starting with the current one.

        Args:
            name: Compute function name
            param_data: Compute function parameters. Defaults to None.

        Returns:
            True if successful, False otherwise
        """
        if param_data is None:
            param = None
        else:
            param = utils.rpcjson_to_dataset(param_data)
        self.SIG_CALC.emit(name, param)
        return True

    @remote_call
    def get_group_titles_with_object_info(
        self,
    ) -> tuple[list[str], list[list[str]], list[list[str]]]:
        """Return groups titles and lists of inner objects uuids and titles.

        Returns:
            Groups titles, lists of inner objects uuids and titles
        """
        return self.win.get_group_titles_with_object_info()

    @remote_call
    def get_object_titles(self, panel: str | None = None) -> list[str]:
        """Get object (signal/image) list for current panel.
        Objects are sorted by group number and object index in group.

        Args:
            panel: panel name (valid values: "signal", "image", "macro").
             If None, current data panel is used (i.e. signal or image panel).

        Returns:
            List of object titles
        """
        return self.win.get_object_titles(panel)

    @remote_call
    def get_object(
        self,
        nb_id_title: int | str | None = None,
        panel: str | None = None,
    ) -> list[str]:
        """Get object (signal/image) from index.

        Args:
            nb_id_title: Object number, or object id, or object title.
             Defaults to None (current object).
            panel: Panel name. Defaults to None (current panel).

        Returns:
            Object

        Raises:
            KeyError: if object not found
        """
        obj = self.win.get_object(nb_id_title, panel)
        if obj is None:
            return None
        return utils.dataset_to_rpcjson(obj)

    @remote_call
    def get_object_uuids(
        self, panel: str | None = None, group: int | str | None = None
    ) -> list[str]:
        """Get object (signal/image) uuid list for current panel.
        Objects are sorted by group number and object index in group.

        Args:
            panel: panel name (valid values: "signal", "image").
             If None, current panel is used.
            group: Group number, or group id, or group title.
             Defaults to None (all groups).

        Returns:
            Object uuids
        """
        return self.win.get_object_uuids(panel, group)

    @remote_call
    def get_object_shapes(
        self,
        nb_id_title: int | str | None = None,
        panel: str | None = None,
    ) -> list:
        """Get plot item shapes associated to object (signal/image).

        Args:
            nb_id_title: Object number, or object id, or object title.
             Defaults to None (current object).
            panel: Panel name. Defaults to None (current panel).

        Returns:
            List of plot item shapes
        """
        items = self.win.get_object_shapes(nb_id_title, panel)
        return items_to_json(items)

    @remote_call
    def add_annotations_from_items(
        self, items_json: str, refresh_plot: bool = True, panel: str | None = None
    ) -> None:
        """Add object annotations (annotation plot items).

        Args:
            items_json: JSON string of annotation items
            refresh_plot: refresh plot. Defaults to True.
            panel: panel name (valid values: "signal", "image").
             If None, current panel is used.
        """
        items = json_to_items(items_json)
        if items:
            self.win.add_annotations_from_items(items, refresh_plot, panel)

    @remote_call
    def add_label_with_title(
        self, title: str | None = None, panel: str | None = None
    ) -> None:
        """Add a label with object title on the associated plot

        Args:
            title: Label title. Defaults to None.
             If None, the title is the object title.
            panel: panel name (valid values: "signal", "image").
             If None, current panel is used.
        """
        self.win.add_label_with_title(title, panel)

    @remote_call
    def run_macro(self, number_or_title: int | str | None = None) -> None:
        """Run macro.

        Args:
            number: Number of the macro (starting at 1). Defaults to None (run
             current macro, or does nothing if there is no macro).
        """
        self.SIG_RUN_MACRO.emit(number_or_title)

    @remote_call
    def stop_macro(self, number_or_title: int | str | None = None) -> None:
        """Stop macro.

        Args:
            number: Number of the macro (starting at 1). Defaults to None (stop
             current macro, or does nothing if there is no macro).
        """
        self.SIG_STOP_MACRO.emit(number_or_title)

    @remote_call
    def import_macro_from_file(self, filename: str) -> None:
        """Import macro from file

        Args:
            filename: Filename.
        """
        self.SIG_IMPORT_MACRO_FROM_FILE.emit(filename)


RemoteServer.check_remote_functions()


# === Python 2.7 client side:
#
# # See doc/remotecontrol_py27.py for an almost complete Python 2.7
# # implementation of RemoteClient class
#
# import io
# from xmlrpclib import ServerProxy, Binary
# import numpy as np
# def array_to_binary(data):
#     """Convert NumPy array to XML-RPC Binary object, with shape and dtype"""
#     dbytes = io.BytesIO()
#     np.save(dbytes, data, allow_pickle=False)
#     return Binary(dbytes.getvalue())
# s = ServerProxy("http://127.0.0.1:8000")
# data = np.array([[3, 4, 5], [7, 8, 0]], dtype=np.uint16)
# s.add_image("toto", array_to_binary(data))


def get_datalab_xmlrpc_port() -> str:
    """Return DataLab current XML-RPC port

    Returns:
        XML-RPC port

    Raises:
        ConnectionRefusedError: DataLab has not yet been executed
    """
    #  The following is valid only when using Python 3.9+ with DataLab
    #  installed on the client side. In any other situation, please use the
    #  ``get_datalab_xmlrpc_port`` function from doc/remotecontrol_py27.py.
    initialize()
    try:
        return Conf.main.rpc_server_port.get()
    except RuntimeError as exc:
        raise ConnectionRefusedError("DataLab has not yet been executed") from exc


[docs] class RemoteClient(BaseProxy): """Object representing a proxy/client to DataLab XML-RPC server. This object is used to call DataLab functions from a Python script. Examples: Here is a simple example of how to use RemoteClient in a Python script or in a Jupyter notebook: >>> from datalab.remote import RemoteClient >>> proxy = RemoteClient() >>> proxy.connect() Connecting to DataLab XML-RPC server...OK (port: 28867) >>> proxy.get_version() '1.0.0' >>> proxy.add_signal("toto", np.array([1., 2., 3.]), np.array([4., 5., -1.])) True >>> proxy.get_object_titles() ['toto'] >>> proxy["toto"] <sigima.objects.signal.SignalObj at 0x7f7f1c0b4a90> >>> proxy[1] <sigima.objects.signal.SignalObj at 0x7f7f1c0b4a90> >>> proxy[1].data array([1., 2., 3.]) """ def __init__(self) -> None: super().__init__() self.port: str | None = None self._datalab: ServerProxy
[docs] def set_port(self, port: str | None = None) -> None: """Set XML-RPC port to connect to. Args: port: XML-RPC port to connect to. If None, the port is automatically retrieved from DataLab configuration. """ execenv.print(f"Setting XML-RPC port... [input:{port}] ", end="") port_str = "" if port is None: port = execenv.xmlrpcport port_str = f"→[execenv.xmlrpcport:{port}] " if port is None: port = get_datalab_xmlrpc_port() port_str = f"→[Conf.main.rpc_server_port:{port}] " execenv.print(port_str, end="") self.port = port if port is None: execenv.print("KO") raise ConnectionRefusedError("DataLab XML-RPC port is not set") execenv.print("OK")
def __connect_to_server(self) -> None: """Connect to DataLab XML-RPC server. Args: port: XML-RPC port to connect to. Raises: ConnectionRefusedError: DataLab is currently not running """ self._datalab = ServerProxy(f"http://127.0.0.1:{self.port}", allow_none=True) try: version = self.get_version() except ConnectionRefusedError as exc: raise ConnectionRefusedError("DataLab is currently not running") from exc # If DataLab version is not compatible with this client, show a warning server_ver = Version(version) client_ver = Version(datalab.__version__) if server_ver < client_ver: warnings.warn( f"DataLab server version ({server_ver}) may not be fully compatible " f"with this DataLab client version ({client_ver}).\n" f"Please upgrade the server to {client_ver} or higher." )
[docs] def connect( self, port: str | None = None, timeout: float | None = None, retries: int | None = None, ) -> None: """Try to connect to DataLab XML-RPC server. Args: port: XML-RPC port to connect to. If not specified, the port is automatically retrieved from DataLab configuration. timeout: Maximum time to wait for connection in seconds. Defaults to 5.0. This is the total maximum wait time, not per retry. retries: Number of retries. Defaults to 10. This parameter is deprecated and will be removed in a future version (kept for backward compatibility). Raises: ConnectionRefusedError: Unable to connect to DataLab ValueError: Invalid timeout (must be >= 0.0) ValueError: Invalid number of retries (must be >= 1) """ timeout = 5.0 if timeout is None else timeout retries = 10 if retries is None else retries # Kept for backward compatibility if timeout < 0.0: raise ValueError("timeout must be >= 0.0") if retries < 1: raise ValueError("retries must be >= 1") execenv.print(f"Connecting to DataLab XML-RPC server... [port:{port}] ", end="") # Use exponential backoff for more efficient retrying start_time = time.time() poll_interval = 0.1 # Start with 100ms max_poll_interval = 1.0 # Cap at 1 second while True: try: # Try to set the port - this may fail if DataLab hasn't written its # config file yet, so we retry it in the loop self.set_port(port) self.__connect_to_server() elapsed = time.time() - start_time execenv.print(f"OK (port: {self.port}, connected in {elapsed:.1f}s)") return except (ConnectionRefusedError, OSError) as exc: # Catch both ConnectionRefusedError and OSError (includes socket errors) elapsed = time.time() - start_time if elapsed >= timeout: execenv.print("KO") raise ConnectionRefusedError( f"Unable to connect to DataLab after {elapsed:.1f}s" ) from exc # Wait before next retry with exponential backoff time.sleep(poll_interval) poll_interval = min(poll_interval * 1.5, max_poll_interval)
[docs] def disconnect(self) -> None: """Disconnect from DataLab XML-RPC server.""" # This is not mandatory with XML-RPC, but if we change protocol in the # future, it may be useful to have a disconnect method. self._datalab = None
[docs] def is_connected(self) -> bool: """Return True if connected to DataLab XML-RPC server.""" if self._datalab is not None: try: self.get_version() return True except ConnectionRefusedError: self._datalab = None return False
[docs] def get_method_list(self) -> list[str]: """Return list of available methods.""" return self._datalab.system.listMethods()
# === Following methods should match the register functions in XML-RPC server
[docs] def add_signal( self, title: str, xdata: np.ndarray, ydata: np.ndarray, xunit: str = "", yunit: str = "", xlabel: str = "", ylabel: str = "", group_id: str = "", set_current: bool = True, ) -> bool: # pylint: disable=too-many-arguments """Add signal data to DataLab. Args: title: Signal title xdata: X data ydata: Y data xunit: X unit. Defaults to "" yunit: Y unit. Defaults to "" xlabel: X label. Defaults to "" ylabel: Y label. Defaults to "" group_id: group id in which to add the signal. Defaults to "" set_current: if True, set the added signal as current Returns: True if signal was added successfully, False otherwise Raises: ValueError: Invalid xdata dtype ValueError: Invalid ydata dtype """ obj = SignalObj() obj.set_xydata(xdata, ydata) obj.check_data() xbinary = utils.array_to_rpcbinary(xdata) ybinary = utils.array_to_rpcbinary(ydata) return self._datalab.add_signal( title, xbinary, ybinary, xunit, yunit, xlabel, ylabel, group_id, set_current )
[docs] def add_image( self, title: str, data: np.ndarray, xunit: str = "", yunit: str = "", zunit: str = "", xlabel: str = "", ylabel: str = "", zlabel: str = "", group_id: str = "", set_current: bool = True, ) -> bool: # pylint: disable=too-many-arguments """Add image data to DataLab. Args: title: Image title data: Image data xunit: X unit. Defaults to "" yunit: Y unit. Defaults to "" zunit: Z unit. Defaults to "" xlabel: X label. Defaults to "" ylabel: Y label. Defaults to "" zlabel: Z label. Defaults to "" group_id: group id in which to add the image. Defaults to "" set_current: if True, set the added image as current Returns: True if image was added successfully, False otherwise Raises: ValueError: Invalid data dtype """ obj = ImageObj() obj.data = data obj.check_data() zbinary = utils.array_to_rpcbinary(data) return self._datalab.add_image( title, zbinary, xunit, yunit, zunit, xlabel, ylabel, zlabel, group_id, set_current, )
[docs] def add_object( self, obj: SignalObj | ImageObj, group_id: str = "", set_current: bool = True, ) -> None: """Add object to DataLab. Args: obj: Signal or image object group_id: group id in which to add the object. Defaults to "" set_current: if True, set the added object as current """ obj_data = utils.dataset_to_rpcjson(obj) self._datalab.add_object(obj_data, group_id, set_current)
[docs] def calc(self, name: str, param: gds.DataSet | None = None) -> None: """Call computation feature ``name`` .. note:: This calls either the processor's ``compute_<name>`` method (if it exists), or the processor's ``<name>`` computation feature (if it is registered, using the ``run_feature`` method). It looks for the function in all panels, starting with the current one. Args: name: Compute function name param: Compute function parameter. Defaults to None. Raises: ValueError: unknown function """ if param is None: return self._datalab.calc(name) return self._datalab.calc(name, utils.dataset_to_rpcjson(param))
[docs] def get_object( self, nb_id_title: int | str | None = None, panel: str | None = None, ) -> SignalObj | ImageObj: """Get object (signal/image) from index. Args: nb_id_title: Object number, or object id, or object title. Defaults to None (current object). panel: Panel name. Defaults to None (current panel). Returns: Object Raises: KeyError: if object not found """ param_data = self._datalab.get_object(nb_id_title, panel) if param_data is None: return None return utils.rpcjson_to_dataset(param_data)
[docs] def get_object_shapes( self, nb_id_title: int | str | None = None, panel: str | None = None, ) -> list: """Get plot item shapes associated to object (signal/image). Args: nb_id_title: Object number, or object id, or object title. Defaults to None (current object). panel: Panel name. Defaults to None (current panel). Returns: List of plot item shapes """ items_json = self._datalab.get_object_shapes(nb_id_title, panel) return json_to_items(items_json)
[docs] def add_annotations_from_items( self, items: list, refresh_plot: bool = True, panel: str | None = None ) -> None: """Add object annotations (annotation plot items). Args: items: annotation plot items refresh_plot: refresh plot. Defaults to True. panel: panel name (valid values: "signal", "image"). If None, current panel is used. """ items_json = items_to_json(items) if items_json is not None: self._datalab.add_annotations_from_items(items_json, refresh_plot, panel)