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 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)

View File

@ -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"],