mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-21 00:40:01 +02:00
Revert "Fix/connection toast timeout"
This commit is contained in:
parent
c5beee5d50
commit
b32bdabfca
@ -109,6 +109,7 @@ const reducer = (state: State, action: Action): State => {
|
|||||||
throw new Error();
|
throw new Error();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [state, dispatch] = useReducer(reducer, null);
|
const [state, dispatch] = useReducer(reducer, null);
|
||||||
const stateRef = useRef(state); // Declare a reference to hold the current state
|
const stateRef = useRef(state); // Declare a reference to hold the current state
|
||||||
@ -149,11 +150,7 @@ const App = () => {
|
|||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
setConnectionStatus('disconnected');
|
setConnectionStatus('disconnected');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Only set "reconnecting" is the state is still "disconnected"
|
setConnectionStatus('reconnecting');
|
||||||
// E.g. when the client has already reconnected
|
|
||||||
setConnectionStatus((currentState) =>
|
|
||||||
currentState === 'disconnected' ? 'reconnecting' : currentState
|
|
||||||
);
|
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -28,7 +28,9 @@ export const DataServiceComponent = React.memo(
|
|||||||
if (name) {
|
if (name) {
|
||||||
fullAccessPath = parentPath.concat('.' + name);
|
fullAccessPath = parentPath.concat('.' + name);
|
||||||
}
|
}
|
||||||
|
console.log(fullAccessPath);
|
||||||
const id = getIdFromFullAccessPath(fullAccessPath);
|
const id = getIdFromFullAccessPath(fullAccessPath);
|
||||||
|
console.log(id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dataServiceComponent" id={id}>
|
<div className="dataServiceComponent" id={id}>
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
from typing import TYPE_CHECKING, Any, Optional
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pydase.data_service.callback_manager import CallbackManager
|
from .callback_manager import CallbackManager
|
||||||
from pydase.data_service.data_service import DataService
|
from .data_service import DataService
|
||||||
from pydase.data_service.state_manager import StateManager
|
from .task_manager import TaskManager
|
||||||
from pydase.data_service.task_manager import TaskManager
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractDataService(ABC):
|
class AbstractDataService(ABC):
|
||||||
__root__: DataService
|
__root__: DataService
|
||||||
_task_manager: TaskManager
|
_task_manager: TaskManager
|
||||||
_callback_manager: CallbackManager
|
_callback_manager: CallbackManager
|
||||||
_state_manager: StateManager
|
|
||||||
_autostart_tasks: dict[str, tuple[Any]]
|
_autostart_tasks: dict[str, tuple[Any]]
|
||||||
_filename: Optional[str]
|
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Optional, get_type_hints
|
from typing import Any, Optional, cast, get_type_hints
|
||||||
|
|
||||||
import rpyc
|
import rpyc
|
||||||
|
|
||||||
import pydase.units as u
|
import pydase.units as u
|
||||||
from pydase.data_service.abstract_data_service import AbstractDataService
|
from pydase.data_service.abstract_data_service import AbstractDataService
|
||||||
from pydase.data_service.callback_manager import CallbackManager
|
from pydase.data_service.callback_manager import CallbackManager
|
||||||
from pydase.data_service.state_manager import StateManager
|
|
||||||
from pydase.data_service.task_manager import TaskManager
|
from pydase.data_service.task_manager import TaskManager
|
||||||
from pydase.utils.helpers import (
|
from pydase.utils.helpers import (
|
||||||
convert_arguments_to_hinted_types,
|
convert_arguments_to_hinted_types,
|
||||||
@ -40,10 +41,8 @@ def process_callable_attribute(attr: Any, args: dict[str, Any]) -> Any:
|
|||||||
|
|
||||||
class DataService(rpyc.Service, AbstractDataService):
|
class DataService(rpyc.Service, AbstractDataService):
|
||||||
def __init__(self, filename: Optional[str] = None) -> None:
|
def __init__(self, filename: Optional[str] = None) -> None:
|
||||||
self._filename: Optional[str] = filename
|
|
||||||
self._callback_manager: CallbackManager = CallbackManager(self)
|
self._callback_manager: CallbackManager = CallbackManager(self)
|
||||||
self._task_manager = TaskManager(self)
|
self._task_manager = TaskManager(self)
|
||||||
self._state_manager = StateManager(self)
|
|
||||||
|
|
||||||
if not hasattr(self, "_autostart_tasks"):
|
if not hasattr(self, "_autostart_tasks"):
|
||||||
self._autostart_tasks = {}
|
self._autostart_tasks = {}
|
||||||
@ -52,11 +51,12 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
"""Keep track of the root object. This helps to filter the emission of
|
"""Keep track of the root object. This helps to filter the emission of
|
||||||
notifications."""
|
notifications."""
|
||||||
|
|
||||||
|
self._filename: Optional[str] = filename
|
||||||
|
|
||||||
self._callback_manager.register_callbacks()
|
self._callback_manager.register_callbacks()
|
||||||
self.__check_instance_classes()
|
self.__check_instance_classes()
|
||||||
self._initialised = True
|
self._initialised = True
|
||||||
|
self._load_values_from_json()
|
||||||
self._state_manager.load_state()
|
|
||||||
|
|
||||||
def __setattr__(self, __name: str, __value: Any) -> None:
|
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||||
# converting attributes that are not properties
|
# converting attributes that are not properties
|
||||||
@ -129,6 +129,15 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
# allow all other attributes
|
# allow all other attributes
|
||||||
setattr(self, name, value)
|
setattr(self, name, value)
|
||||||
|
|
||||||
|
def _load_values_from_json(self) -> None:
|
||||||
|
if self._filename is not None:
|
||||||
|
# Check if the file specified by the filename exists
|
||||||
|
if os.path.exists(self._filename):
|
||||||
|
with open(self._filename, "r") as f:
|
||||||
|
# Load JSON data from file and update class attributes with these
|
||||||
|
# values
|
||||||
|
self.load_DataService_from_JSON(cast(dict[str, Any], json.load(f)))
|
||||||
|
|
||||||
def write_to_file(self) -> None:
|
def write_to_file(self) -> None:
|
||||||
"""
|
"""
|
||||||
Serialize the DataService instance and write it to a JSON file.
|
Serialize the DataService instance and write it to a JSON file.
|
||||||
@ -136,8 +145,14 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
Args:
|
Args:
|
||||||
filename (str): The name of the file to write to.
|
filename (str): The name of the file to write to.
|
||||||
"""
|
"""
|
||||||
if self._state_manager is not None:
|
if self._filename is not None:
|
||||||
self._state_manager.save_state()
|
with open(self._filename, "w") as f:
|
||||||
|
json.dump(self.serialize(), f, indent=4)
|
||||||
|
else:
|
||||||
|
logger.error(
|
||||||
|
f"Class {self.__class__.__name__} was not initialised with a filename. "
|
||||||
|
'Skipping "write_to_file"...'
|
||||||
|
)
|
||||||
|
|
||||||
def load_DataService_from_JSON(self, json_dict: dict[str, Any]) -> None:
|
def load_DataService_from_JSON(self, json_dict: dict[str, Any]) -> None:
|
||||||
# Traverse the serialized representation and set the attributes of the class
|
# Traverse the serialized representation and set the attributes of the class
|
||||||
|
@ -1,146 +0,0 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
from typing import TYPE_CHECKING, Any, cast
|
|
||||||
|
|
||||||
import pydase.units as u
|
|
||||||
from pydase.utils.helpers import (
|
|
||||||
generate_paths_from_DataService_dict,
|
|
||||||
get_nested_value_from_DataService_by_path_and_key,
|
|
||||||
set_nested_value_in_dict,
|
|
||||||
)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from pydase import DataService
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class StateManager:
|
|
||||||
"""
|
|
||||||
Manages the state of a DataService instance, serving as both a cache and a
|
|
||||||
persistence layer. It is designed to provide quick access to the latest known state
|
|
||||||
for newly connecting web clients without the need for expensive property accesses
|
|
||||||
that may involve complex calculations or I/O operations.
|
|
||||||
|
|
||||||
The StateManager listens for state change notifications from the DataService's
|
|
||||||
callback manager and updates its cache accordingly. This cache does not always
|
|
||||||
reflect the most current complex property states but rather retains the value from
|
|
||||||
the last known state, optimizing for performance and reducing the load on the
|
|
||||||
system.
|
|
||||||
|
|
||||||
While the StateManager ensures that the cached state is as up-to-date as possible,
|
|
||||||
it does not autonomously update complex properties of the DataService. Such
|
|
||||||
properties must be updated programmatically, for instance, by invoking specific
|
|
||||||
tasks or methods that trigger the necessary operations to refresh their state.
|
|
||||||
|
|
||||||
The cached state maintained by the StateManager is particularly useful for web
|
|
||||||
clients that connect to the system and need immediate access to the current state of
|
|
||||||
the DataService. By avoiding direct and potentially costly property accesses, the
|
|
||||||
StateManager provides a snapshot of the DataService's state that is sufficiently
|
|
||||||
accurate for initial rendering and interaction.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
cache (dict[str, Any]):
|
|
||||||
A dictionary cache of the DataService's state.
|
|
||||||
filename (str):
|
|
||||||
The file name used for storing the DataService's state.
|
|
||||||
service (DataService):
|
|
||||||
The DataService instance whose state is being managed.
|
|
||||||
|
|
||||||
Note:
|
|
||||||
The StateManager's cache updates are triggered by notifications and do not
|
|
||||||
include autonomous updates of complex DataService properties, which must be
|
|
||||||
managed programmatically. The cache serves the purpose of providing immediate
|
|
||||||
state information to web clients, reflecting the state after the last property
|
|
||||||
update.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, service: "DataService"):
|
|
||||||
self.cache: dict[str, Any] = {} # Initialize an empty cache
|
|
||||||
self.filename = service._filename
|
|
||||||
self.service = service
|
|
||||||
self.service._callback_manager.add_notification_callback(self.update_cache)
|
|
||||||
|
|
||||||
def update_cache(self, parent_path: str, name: str, value: Any) -> None:
|
|
||||||
# Remove the part before the first "." in the parent_path
|
|
||||||
parent_path = ".".join(parent_path.split(".")[1:])
|
|
||||||
|
|
||||||
# Construct the full path
|
|
||||||
full_path = f"{parent_path}.{name}" if parent_path else name
|
|
||||||
|
|
||||||
set_nested_value_in_dict(self.cache, full_path, value)
|
|
||||||
|
|
||||||
def save_state(self) -> None:
|
|
||||||
"""
|
|
||||||
Serialize the DataService instance and write it to a JSON file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filename (str): The name of the file to write to.
|
|
||||||
"""
|
|
||||||
if self.filename is not None:
|
|
||||||
with open(self.filename, "w") as f:
|
|
||||||
json.dump(self.cache, f, indent=4)
|
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Class {self.__class__.__name__} was not initialised with a filename. "
|
|
||||||
'Skipping "write_to_file"...'
|
|
||||||
)
|
|
||||||
|
|
||||||
def load_state(self) -> None:
|
|
||||||
# Traverse the serialized representation and set the attributes of the class
|
|
||||||
if self.cache == {}:
|
|
||||||
self.cache = self.service.serialize()
|
|
||||||
|
|
||||||
json_dict = self._load_state_from_file()
|
|
||||||
if json_dict == {}:
|
|
||||||
logger.debug("Could not load the service state.")
|
|
||||||
return
|
|
||||||
|
|
||||||
serialized_class = self.cache
|
|
||||||
for path in generate_paths_from_DataService_dict(json_dict):
|
|
||||||
value = get_nested_value_from_DataService_by_path_and_key(
|
|
||||||
json_dict, path=path
|
|
||||||
)
|
|
||||||
value_type = get_nested_value_from_DataService_by_path_and_key(
|
|
||||||
json_dict, path=path, key="type"
|
|
||||||
)
|
|
||||||
class_value_type = get_nested_value_from_DataService_by_path_and_key(
|
|
||||||
serialized_class, path=path, key="type"
|
|
||||||
)
|
|
||||||
if class_value_type == value_type:
|
|
||||||
class_attr_is_read_only = (
|
|
||||||
get_nested_value_from_DataService_by_path_and_key(
|
|
||||||
serialized_class, path=path, key="readonly"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if class_attr_is_read_only:
|
|
||||||
logger.debug(
|
|
||||||
f'Attribute "{path}" is read-only. Ignoring value from JSON '
|
|
||||||
"file..."
|
|
||||||
)
|
|
||||||
continue
|
|
||||||
# Split the path into parts
|
|
||||||
parts = path.split(".")
|
|
||||||
attr_name = parts[-1]
|
|
||||||
|
|
||||||
# Convert dictionary into Quantity
|
|
||||||
if class_value_type == "Quantity":
|
|
||||||
value = u.convert_to_quantity(value)
|
|
||||||
|
|
||||||
self.service.update_DataService_attribute(parts[:-1], attr_name, value)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f'Attribute type of "{path}" changed from "{value_type}" to '
|
|
||||||
f'"{class_value_type}". Ignoring value from JSON file...'
|
|
||||||
)
|
|
||||||
|
|
||||||
def _load_state_from_file(self) -> dict[str, Any]:
|
|
||||||
if self.filename is not None:
|
|
||||||
# Check if the file specified by the filename exists
|
|
||||||
if os.path.exists(self.filename):
|
|
||||||
with open(self.filename, "r") as f:
|
|
||||||
# Load JSON data from file and update class attributes with these
|
|
||||||
# values
|
|
||||||
return cast(dict[str, Any], json.load(f))
|
|
||||||
return {}
|
|
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.c444b055.css",
|
"main.css": "/static/css/main.c444b055.css",
|
||||||
"main.js": "/static/js/main.16698eff.js",
|
"main.js": "/static/js/main.964bc334.js",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.c444b055.css.map": "/static/css/main.c444b055.css.map",
|
"main.c444b055.css.map": "/static/css/main.c444b055.css.map",
|
||||||
"main.16698eff.js.map": "/static/js/main.16698eff.js.map"
|
"main.964bc334.js.map": "/static/js/main.964bc334.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.c444b055.css",
|
"static/css/main.c444b055.css",
|
||||||
"static/js/main.16698eff.js"
|
"static/js/main.964bc334.js"
|
||||||
]
|
]
|
||||||
}
|
}
|
@ -1 +1 @@
|
|||||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.16698eff.js"></script><link href="/static/css/main.c444b055.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.964bc334.js"></script><link href="/static/css/main.c444b055.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/pydase/frontend/static/js/main.964bc334.js.map
Normal file
1
src/pydase/frontend/static/js/main.964bc334.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -323,8 +323,8 @@ class Server:
|
|||||||
logger.info("Shutting down")
|
logger.info("Shutting down")
|
||||||
|
|
||||||
logger.info(f"Saving data to {self._service._filename}.")
|
logger.info(f"Saving data to {self._service._filename}.")
|
||||||
if self._service._state_manager is not None:
|
if self._service._filename is not None:
|
||||||
self._service._state_manager.save_state()
|
self._service.write_to_file()
|
||||||
|
|
||||||
await self.__cancel_servers()
|
await self.__cancel_servers()
|
||||||
await self.__cancel_tasks()
|
await self.__cancel_tasks()
|
||||||
|
@ -114,7 +114,7 @@ class WebAPI:
|
|||||||
|
|
||||||
@app.get("/service-properties")
|
@app.get("/service-properties")
|
||||||
def service_properties() -> dict[str, Any]:
|
def service_properties() -> dict[str, Any]:
|
||||||
return self.service._state_manager.cache
|
return self.service.serialize()
|
||||||
|
|
||||||
# exposing custom.css file provided by user
|
# exposing custom.css file provided by user
|
||||||
if self.css is not None:
|
if self.css is not None:
|
||||||
|
@ -272,90 +272,6 @@ def get_nested_value_from_DataService_by_path_and_key(
|
|||||||
return current_data.get(key, None)
|
return current_data.get(key, None)
|
||||||
|
|
||||||
|
|
||||||
def set_nested_value_in_dict(data_dict: dict[str, Any], path: str, value: Any) -> None:
|
|
||||||
"""
|
|
||||||
Set the value associated with a specific key in a dictionary given a path.
|
|
||||||
|
|
||||||
This function traverses the dictionary according to the path provided and
|
|
||||||
sets the value at that path. The path is a string with dots connecting
|
|
||||||
the levels and brackets indicating list indices.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cache (dict): The cache dictionary to set the value in.
|
|
||||||
path (str): The path to where the value should be set in the dictionary.
|
|
||||||
value (Any): The value to be set at the specified path in the dictionary.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
Let's consider the following dictionary:
|
|
||||||
|
|
||||||
cache = {
|
|
||||||
"attr1": {"type": "int", "value": 10},
|
|
||||||
"attr2": {
|
|
||||||
"type": "MyClass",
|
|
||||||
"value": {"attr3": {"type": "float", "value": 20.5}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
The function can be used to set the value of 'attr1' as follows:
|
|
||||||
set_nested_value_in_cache(cache, "attr1", 15)
|
|
||||||
|
|
||||||
It can also be used to set the value of 'attr3', which is nested within 'attr2',
|
|
||||||
as follows:
|
|
||||||
set_nested_value_in_cache(cache, "attr2.attr3", 25.0)
|
|
||||||
"""
|
|
||||||
|
|
||||||
parts = path.split(".")
|
|
||||||
current_dict: dict[str, Any] = data_dict
|
|
||||||
index: Optional[int] = None
|
|
||||||
|
|
||||||
for attr_name in parts:
|
|
||||||
# Check if the key contains an index part like '[<index>]'
|
|
||||||
if "[" in attr_name and attr_name.endswith("]"):
|
|
||||||
attr_name, index_part = attr_name.split("[", 1)
|
|
||||||
index_part = index_part.rstrip("]") # remove the closing bracket
|
|
||||||
|
|
||||||
# Convert the index part to an integer
|
|
||||||
if index_part.isdigit():
|
|
||||||
index = int(index_part)
|
|
||||||
else:
|
|
||||||
logger.error(f"Invalid index format in key: {attr_name}")
|
|
||||||
|
|
||||||
current_dict = cast(dict[str, Any], current_dict.get(attr_name, None))
|
|
||||||
|
|
||||||
if not isinstance(current_dict, dict):
|
|
||||||
# key does not exist in dictionary, e.g. when class does not have this
|
|
||||||
# attribute
|
|
||||||
return
|
|
||||||
|
|
||||||
if index is not None:
|
|
||||||
if 0 <= index < len(current_dict["value"]):
|
|
||||||
try:
|
|
||||||
current_dict = cast(dict[str, Any], current_dict["value"][index])
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Could not change {path}. Exception: {e}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# TODO: appending to a list will probably be done here
|
|
||||||
logger.error(f"Could not change {path}...")
|
|
||||||
return
|
|
||||||
|
|
||||||
# When the attribute is a class instance, the attributes are nested in the
|
|
||||||
# "value" key
|
|
||||||
if (
|
|
||||||
current_dict["type"] not in STANDARD_TYPES
|
|
||||||
and current_dict["type"] != "method"
|
|
||||||
):
|
|
||||||
current_dict = cast(dict[str, Any], current_dict.get("value", None)) # type: ignore
|
|
||||||
|
|
||||||
index = None
|
|
||||||
|
|
||||||
# setting the new value
|
|
||||||
try:
|
|
||||||
current_dict["value"] = value
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(e)
|
|
||||||
|
|
||||||
|
|
||||||
def convert_arguments_to_hinted_types(
|
def convert_arguments_to_hinted_types(
|
||||||
args: dict[str, Any], type_hints: dict[str, Any]
|
args: dict[str, Any], type_hints: dict[str, Any]
|
||||||
) -> dict[str, Any] | str:
|
) -> dict[str, Any] | str:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user