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
This commit is contained in:
Mose Müller 2024-03-28 18:29:37 +01:00
parent 36a70badce
commit d100bb5fea
2 changed files with 92 additions and 44 deletions

View File

@ -1,15 +1,14 @@
import logging import logging
import time import time
from typing import TYPE_CHECKING, TypedDict from typing import Any, TypedDict
import socketio # type: ignore 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.deserializer import loads
from pydase.utils.serialization.serializer import SerializedObject from pydase.utils.serialization.serializer import SerializedObject, dump
from pydase.utils.serialization.types import SerializedDataService
if TYPE_CHECKING:
from pydase.client.proxy_class_factory import ProxyClass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,31 +22,56 @@ class NotifyDict(TypedDict):
data: NotifyDataDict data: NotifyDataDict
class Client: class Client(pydase.DataService):
def __init__(self, hostname: str, port: int): def __init__(self, hostname: str, port: int):
self.sio = socketio.Client() super().__init__()
self.setup_events() self._sio = socketio.Client()
self.proxy_class_factory = ProxyClassFactory(self.sio) self._setup_events()
self.proxy: ProxyClass | None = None self._proxy_class_factory = ProxyClassFactory(self._sio)
self.sio.connect( self.proxy = ProxyConnection()
self._sio.connect(
f"ws://{hostname}:{port}", f"ws://{hostname}:{port}",
socketio_path="/ws/socket.io", socketio_path="/ws/socket.io",
transports=["websocket"], transports=["websocket"],
) )
while self.proxy is None: while not self.proxy._initialised:
time.sleep(0.01) time.sleep(0.01)
def setup_events(self) -> None: def _setup_events(self) -> None:
@self.sio.event @self._sio.event
def class_structure(data: SerializedObject) -> None: def class_structure(data: SerializedDataService) -> None:
self.proxy = self.proxy_class_factory.create_proxy(data) 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: def notify(data: NotifyDict) -> None:
if self.proxy is not None: # Notify the DataServiceObserver directly, not going through
self.proxy._notify_changed( # self._notify_changed as this would trigger the "update_value" event
data["data"]["full_access_path"], loads(data["data"]["value"]) super(pydase.DataService, self)._notify_changed(
) f"proxy.{data['data']['full_access_path']}",
loads(data["data"]["value"]),
)
def disconnect(self) -> None: 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)

View File

@ -1,10 +1,14 @@
import logging import logging
from collections.abc import Callable
from copy import copy from copy import copy
from typing import TYPE_CHECKING, Any, cast from typing import Any, cast
import socketio # type: ignore import socketio # type: ignore
import pydase 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.deserializer import Deserializer, loads
from pydase.utils.serialization.serializer import ( from pydase.utils.serialization.serializer import (
SerializedMethod, SerializedMethod,
@ -12,31 +16,48 @@ from pydase.utils.serialization.serializer import (
dump, dump,
) )
if TYPE_CHECKING: logger = logging.getLogger(__name__)
from collections.abc import Callable
class ProxyClass(pydase.DataService): class ProxyClassMixin:
__sio: socketio.Client _sio: socketio.Client
def __setattr__(self, key, value): def __setattr__(self, key: str, value: Any) -> None:
# prevent overriding of proxy attributes # 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.") raise AttributeError(f"{key} is read-only and cannot be overridden.")
super().__setattr__(key, value) 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: class ProxyClassFactory:
def __init__(self, sio_client: socketio.Client) -> None: def __init__(self, sio_client: socketio.Client) -> None:
self.sio_client = sio_client self.sio_client = sio_client
def create_proxy(self, data: SerializedObject) -> ProxyClass: def create_proxy(self, data: SerializedObject) -> ProxyConnection:
proxy: ProxyClass = self._deserialize(data) proxy_class = self._deserialize_component_type(data, ProxyConnection)
return proxy proxy_class._sio = self.sio_client
proxy_class._initialised = True
return proxy_class # type: ignore
def _deserialize(self, serialized_object: SerializedObject) -> Any: def _deserialize(self, serialized_object: SerializedObject) -> Any:
type_handler: dict[str | None, None | Callable[..., Any]] = { type_handler: dict[str | None, None | Callable[..., Any]] = {
@ -65,15 +86,18 @@ class ProxyClassFactory:
proxy_class = self._deserialize_component_type( proxy_class = self._deserialize_component_type(
serialized_object, component_class serialized_object, component_class
) )
proxy_class.__sio = self.sio_client proxy_class._sio = self.sio_client
proxy_class._initialised = True
return proxy_class return proxy_class
return None return None
def _deserialize_method(self, serialized_object: SerializedMethod) -> Any: def _deserialize_method(
def method_proxy(self: ProxyClass, *args: Any, **kwargs: Any) -> Any: self, serialized_object: SerializedMethod
) -> Callable[..., Any]:
def method_proxy(self: ProxyBaseClass, *args: Any, **kwargs: Any) -> Any:
serialized_response = cast( serialized_response = cast(
dict[str, Any], dict[str, Any],
self.__sio.call( self._sio.call(
"trigger_method", "trigger_method",
{ {
"access_path": serialized_object["full_access_path"], "access_path": serialized_object["full_access_path"],
@ -88,7 +112,7 @@ class ProxyClassFactory:
def _deserialize_component_type( def _deserialize_component_type(
self, serialized_object: SerializedObject, base_class: type self, serialized_object: SerializedObject, base_class: type
) -> ProxyClass: ) -> pydase.DataService:
def add_prefix_to_last_path_element(s: str, prefix: str) -> str: def add_prefix_to_last_path_element(s: str, prefix: str) -> str:
parts = s.split(".") parts = s.split(".")
parts[-1] = f"{prefix}_{parts[-1]}" parts[-1] = f"{prefix}_{parts[-1]}"
@ -96,7 +120,7 @@ class ProxyClassFactory:
def create_proxy_class(serialized_object: SerializedObject) -> type: def create_proxy_class(serialized_object: SerializedObject) -> type:
class_bases = ( class_bases = (
ProxyClass, ProxyBaseClass,
base_class, base_class,
) )
class_attrs: dict[str, Any] = {} class_attrs: dict[str, Any] = {}
@ -136,20 +160,20 @@ class ProxyClassFactory:
return create_proxy_class(serialized_object)() return create_proxy_class(serialized_object)()
def _create_attr_property(self, serialized_attr: SerializedObject) -> property: 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( return loads(
cast( cast(
SerializedObject, 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"] 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( result = cast(
SerializedObject | None, SerializedObject | None,
self.__sio.call( self._sio.call(
"update_value", "update_value",
{ {
"access_path": serialized_attr["full_access_path"], "access_path": serialized_attr["full_access_path"],