From d100bb5fea1c4b9af21457bc83b1c7b757f491bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Thu, 28 Mar 2024 18:29:37 +0100 Subject: [PATCH] udpates Client and ProxyClassFactory - Client: - inherits from DataService now - acts as an observer of the proxy class and sends updates to the sio server - ProxyClassFactory - ProxyConnection is now a DeviceConnection -> users will see if the client is connected --- src/pydase/client/client.py | 70 ++++++++++++++++-------- src/pydase/client/proxy_class_factory.py | 66 +++++++++++++++------- 2 files changed, 92 insertions(+), 44 deletions(-) diff --git a/src/pydase/client/client.py b/src/pydase/client/client.py index 6811495..ca8e802 100644 --- a/src/pydase/client/client.py +++ b/src/pydase/client/client.py @@ -1,15 +1,14 @@ import logging import time -from typing import TYPE_CHECKING, TypedDict +from typing import Any, TypedDict import socketio # type: ignore -from pydase.client.proxy_class_factory import ProxyClassFactory +import pydase +from pydase.client.proxy_class_factory import ProxyClassFactory, ProxyConnection from pydase.utils.serialization.deserializer import loads -from pydase.utils.serialization.serializer import SerializedObject - -if TYPE_CHECKING: - from pydase.client.proxy_class_factory import ProxyClass +from pydase.utils.serialization.serializer import SerializedObject, dump +from pydase.utils.serialization.types import SerializedDataService logger = logging.getLogger(__name__) @@ -23,31 +22,56 @@ class NotifyDict(TypedDict): data: NotifyDataDict -class Client: +class Client(pydase.DataService): def __init__(self, hostname: str, port: int): - self.sio = socketio.Client() - self.setup_events() - self.proxy_class_factory = ProxyClassFactory(self.sio) - self.proxy: ProxyClass | None = None - self.sio.connect( + super().__init__() + self._sio = socketio.Client() + self._setup_events() + self._proxy_class_factory = ProxyClassFactory(self._sio) + self.proxy = ProxyConnection() + self._sio.connect( f"ws://{hostname}:{port}", socketio_path="/ws/socket.io", transports=["websocket"], ) - while self.proxy is None: + while not self.proxy._initialised: time.sleep(0.01) - def setup_events(self) -> None: - @self.sio.event - def class_structure(data: SerializedObject) -> None: - self.proxy = self.proxy_class_factory.create_proxy(data) + def _setup_events(self) -> None: + @self._sio.event + def class_structure(data: SerializedDataService) -> None: + if not self.proxy._initialised: + self.proxy = self._proxy_class_factory.create_proxy(data) + else: + # need to change to avoid overwriting the proxy class + data["type"] = "DeviceConnection" + self.proxy._notify_changed("", loads(data)) - @self.sio.event + @self._sio.event def notify(data: NotifyDict) -> None: - if self.proxy is not None: - self.proxy._notify_changed( - data["data"]["full_access_path"], loads(data["data"]["value"]) - ) + # Notify the DataServiceObserver directly, not going through + # self._notify_changed as this would trigger the "update_value" event + super(pydase.DataService, self)._notify_changed( + f"proxy.{data['data']['full_access_path']}", + loads(data["data"]["value"]), + ) def disconnect(self) -> None: - self.sio.disconnect() + self._sio.disconnect() + + def _notify_changed(self, changed_attribute: str, value: Any) -> None: + if ( + changed_attribute.startswith("proxy.") + and all(part[0] != "_" for part in changed_attribute.split(".")) + and changed_attribute != "proxy.connected" + ): + logger.debug(f"{changed_attribute}: {value}") + + self._sio.call( + "update_value", + { + "access_path": changed_attribute[6:], + "value": dump(value), + }, + ) + return super()._notify_changed(changed_attribute, value) diff --git a/src/pydase/client/proxy_class_factory.py b/src/pydase/client/proxy_class_factory.py index fe4c2a9..955f287 100644 --- a/src/pydase/client/proxy_class_factory.py +++ b/src/pydase/client/proxy_class_factory.py @@ -1,10 +1,14 @@ import logging +from collections.abc import Callable from copy import copy -from typing import TYPE_CHECKING, Any, cast +from typing import Any, cast import socketio # type: ignore import pydase +import pydase.components +import pydase.observer_pattern.observer +from pydase.utils.helpers import is_property_attribute from pydase.utils.serialization.deserializer import Deserializer, loads from pydase.utils.serialization.serializer import ( SerializedMethod, @@ -12,31 +16,48 @@ from pydase.utils.serialization.serializer import ( dump, ) -if TYPE_CHECKING: - from collections.abc import Callable +logger = logging.getLogger(__name__) -class ProxyClass(pydase.DataService): - __sio: socketio.Client +class ProxyClassMixin: + _sio: socketio.Client - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: # prevent overriding of proxy attributes - if hasattr(self, key) and isinstance(getattr(self, key), ProxyClass): + if ( + not is_property_attribute(self, key) + and hasattr(self, key) + and isinstance(getattr(self, key), ProxyBaseClass) + ): raise AttributeError(f"{key} is read-only and cannot be overridden.") super().__setattr__(key, value) -logger = logging.getLogger(__name__) +class ProxyBaseClass(pydase.DataService, ProxyClassMixin): + pass + + +class ProxyConnection(pydase.components.DeviceConnection, ProxyClassMixin): + def __init__(self) -> None: + super().__init__() + self._initialised = False + self._reconnection_wait_time = 1 + + @property + def connected(self) -> bool: + return self._sio.connected class ProxyClassFactory: def __init__(self, sio_client: socketio.Client) -> None: self.sio_client = sio_client - def create_proxy(self, data: SerializedObject) -> ProxyClass: - proxy: ProxyClass = self._deserialize(data) - return proxy + def create_proxy(self, data: SerializedObject) -> ProxyConnection: + proxy_class = self._deserialize_component_type(data, ProxyConnection) + proxy_class._sio = self.sio_client + proxy_class._initialised = True + return proxy_class # type: ignore def _deserialize(self, serialized_object: SerializedObject) -> Any: type_handler: dict[str | None, None | Callable[..., Any]] = { @@ -65,15 +86,18 @@ class ProxyClassFactory: proxy_class = self._deserialize_component_type( serialized_object, component_class ) - proxy_class.__sio = self.sio_client + proxy_class._sio = self.sio_client + proxy_class._initialised = True return proxy_class return None - def _deserialize_method(self, serialized_object: SerializedMethod) -> Any: - def method_proxy(self: ProxyClass, *args: Any, **kwargs: Any) -> Any: + def _deserialize_method( + self, serialized_object: SerializedMethod + ) -> Callable[..., Any]: + def method_proxy(self: ProxyBaseClass, *args: Any, **kwargs: Any) -> Any: serialized_response = cast( dict[str, Any], - self.__sio.call( + self._sio.call( "trigger_method", { "access_path": serialized_object["full_access_path"], @@ -88,7 +112,7 @@ class ProxyClassFactory: def _deserialize_component_type( self, serialized_object: SerializedObject, base_class: type - ) -> ProxyClass: + ) -> pydase.DataService: def add_prefix_to_last_path_element(s: str, prefix: str) -> str: parts = s.split(".") parts[-1] = f"{prefix}_{parts[-1]}" @@ -96,7 +120,7 @@ class ProxyClassFactory: def create_proxy_class(serialized_object: SerializedObject) -> type: class_bases = ( - ProxyClass, + ProxyBaseClass, base_class, ) class_attrs: dict[str, Any] = {} @@ -136,20 +160,20 @@ class ProxyClassFactory: return create_proxy_class(serialized_object)() def _create_attr_property(self, serialized_attr: SerializedObject) -> property: - def get(self: ProxyClass) -> Any: # type: ignore + def get(self: ProxyBaseClass) -> Any: # type: ignore return loads( cast( SerializedObject, - self.__sio.call("get_value", serialized_attr["full_access_path"]), + self._sio.call("get_value", serialized_attr["full_access_path"]), ) ) get.__doc__ = serialized_attr["doc"] - def set(self: ProxyClass, value: Any) -> None: # type: ignore + def set(self: ProxyBaseClass, value: Any) -> None: # type: ignore result = cast( SerializedObject | None, - self.__sio.call( + self._sio.call( "update_value", { "access_path": serialized_attr["full_access_path"],