20 Commits

Author SHA1 Message Date
Mose Müller
c7d452d7db adds tests for Image component 2024-03-05 16:32:20 +01:00
Mose Müller
b7926b730d updates version to v0.7.3 2024-03-05 16:32:07 +01:00
Mose Müller
0c175fc706 Merge pull request #109 from tiqi-group/fix/task_disappears_after_changing_state
Fix/task disappears after changing state
2024-03-05 16:08:55 +01:00
Mose Müller
7d21bca8b1 adds test for changing task state 2024-03-05 16:05:09 +01:00
Mose Müller
d1628ae8c9 fixes updating task state 2024-03-05 16:05:01 +01:00
Mose Müller
441658ebc1 Merge pull request #108 from tiqi-group/fix/cache_update_on_type_change
Fix/cache update on type change
2024-03-05 14:44:19 +01:00
Mose Müller
99c7ad0ec8 updates serializer tests 2024-03-05 14:28:53 +01:00
Mose Müller
24a01c0982 removes keys from cache entry if they are not part of the new value serialization 2024-03-05 14:17:05 +01:00
Mose Müller
b8a52c2e6a only update cache and execute notification callbacks if attribute is public and has changed 2024-03-05 13:56:02 +01:00
Mose Müller
7aacc21010 removes processing of value from sio_callback (cached value is up-to-date already) 2024-03-05 13:54:24 +01:00
Mose Müller
8787cb0509 get cached value before executing custom notification callbacks 2024-03-05 13:53:41 +01:00
Mose Müller
8971cebfcd adds todos 2024-03-05 13:24:54 +01:00
Mose Müller
f2cf0d9c1a fixes update of cache when the type has changed
When an attribute changes from, say, a quantity to an enumeration, the enum key in the serialization was not added to the
cache, and thus the frontend was not able to render the enum.
2024-03-05 13:23:26 +01:00
Mose Müller
36c863e845 Merge pull request #107 from tiqi-group/fix/update_frontend_before_setting_state
Fix/update frontend before setting state
2024-03-05 13:20:54 +01:00
Mose Müller
836c1e14df npm run build 2024-03-05 13:19:10 +01:00
Mose Müller
dba036c6b3 do not try to update state if it is not yet set
This happens when the backend pushes updates before the frontend has received and set the state when loading the page, first.
2024-03-05 13:19:02 +01:00
Mose Müller
8b1f1ef1b1 updates to version v0.7.2 2024-03-04 17:46:44 +01:00
Mose Müller
698db4881b Merge pull request #106 from tiqi-group/fix/enum_sio_callback
fixes sio callback when attribute changes to an enum which was not present before
2024-03-04 17:38:33 +01:00
Mose Müller
d709d43d75 ignores complexity of sio_server setup (will be changed anyway soon 2024-03-04 17:36:09 +01:00
Mose Müller
691bf809cb fixes sio callback when attribute changes to an enum which was not present before 2024-03-04 17:32:45 +01:00
13 changed files with 239 additions and 29 deletions

View File

@@ -32,6 +32,9 @@ const reducer = (state: State, action: Action): State => {
case 'SET_DATA':
return action.data;
case 'UPDATE_ATTRIBUTE': {
if (state === null) {
return null;
}
return {
...state,
value: setNestedValueByPath(state.value, action.fullAccessPath, action.newValue)

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydase"
version = "0.7.1"
version = "0.7.3"
description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases."
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
readme = "README.md"

View File

@@ -42,10 +42,16 @@ class DataServiceObserver(PropertyObserver):
):
logger.debug("'%s' changed to '%s'", full_access_path, value)
self._update_cache_value(full_access_path, value, cached_value_dict)
self._update_cache_value(full_access_path, value, cached_value_dict)
for callback in self._notification_callbacks:
callback(full_access_path, value, cached_value_dict)
cached_value_dict = deepcopy(
self.state_manager._data_service_cache.get_value_dict_from_cache(
full_access_path
)
)
for callback in self._notification_callbacks:
callback(full_access_path, value, cached_value_dict)
if isinstance(value, ObservableObject):
self._update_property_deps_dict()

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.7ef670d5.css",
"main.js": "/static/js/main.6d1d080e.js",
"main.js": "/static/js/main.ca0b4800.js",
"index.html": "/index.html",
"main.7ef670d5.css.map": "/static/css/main.7ef670d5.css.map",
"main.6d1d080e.js.map": "/static/js/main.6d1d080e.js.map"
"main.ca0b4800.js.map": "/static/js/main.ca0b4800.js.map"
},
"entrypoints": [
"static/css/main.7ef670d5.css",
"static/js/main.6d1d080e.js"
"static/js/main.ca0b4800.js"
]
}

View File

@@ -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.6d1d080e.js"></script><link href="/static/css/main.7ef670d5.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.ca0b4800.js"></script><link href="/static/css/main.7ef670d5.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

View File

@@ -9,7 +9,6 @@ from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager
from pydase.utils.helpers import get_object_attr_from_path_list
from pydase.utils.logging import SocketIOHandler
from pydase.utils.serializer import dump
logger = logging.getLogger(__name__)
@@ -97,11 +96,6 @@ def setup_sio_server(
full_access_path: str, value: Any, cached_value_dict: dict[str, Any]
) -> None:
if cached_value_dict != {}:
serialized_value = dump(value)
if cached_value_dict["type"] != "method":
cached_value_dict["type"] = serialized_value["type"]
cached_value_dict["value"] = serialized_value["value"]
async def notify() -> None:
try:

View File

@@ -267,14 +267,27 @@ def set_nested_value_by_path(
logger.error(e)
return
# setting the new value
serialized_value = dump(value)
if "readonly" in current_dict:
if current_dict["type"] != "method":
current_dict["type"] = serialized_value["type"]
current_dict["value"] = serialized_value["value"]
keys_to_keep = set(serialized_value.keys())
if current_dict == {}: # adding an attribute / element to a list or dict
pass
elif current_dict["type"] == "method": # state change of task
keys_to_keep = set(current_dict.keys())
serialized_value = current_dict
serialized_value["value"] = value.name if isinstance(value, Enum) else None
else:
current_dict.update(serialized_value)
# attribute-specific information should not be overwritten by new value
serialized_value.pop("readonly")
serialized_value.pop("doc")
current_dict.update(serialized_value)
# removes keys that are not present in the serialized new value
for key in list(current_dict.keys()):
if key not in keys_to_keep:
current_dict.pop(key, None)
def get_nested_dict_by_path(

View File

@@ -0,0 +1,141 @@
import logging
import pydase
import pydase.components
from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager
from pydase.utils.serializer import dump
from pytest import LogCaptureFixture
logger = logging.getLogger(__name__)
def test_image_functions(caplog: LogCaptureFixture) -> None:
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.my_image = pydase.components.Image()
service_instance = MyService()
state_manager = StateManager(service_instance)
DataServiceObserver(state_manager)
service_instance.my_image.load_from_url("https://cataas.com/cat")
caplog.clear()
def test_image_serialization() -> None:
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.my_image = pydase.components.Image()
assert dump(MyService()) == {
"name": "MyService",
"type": "DataService",
"value": {
"my_image": {
"name": "Image",
"type": "Image",
"value": {
"format": {
"type": "str",
"value": "",
"readonly": True,
"doc": None,
},
"load_from_base64": {
"type": "method",
"value": None,
"readonly": True,
"doc": None,
"async": False,
"signature": {
"parameters": {
"value_": {
"annotation": "<class 'bytes'>",
"default": {},
},
"format_": {
"annotation": "str | None",
"default": {
"type": "NoneType",
"value": None,
"readonly": False,
"doc": None,
},
},
},
"return_annotation": {},
},
"frontend_render": False,
},
"load_from_matplotlib_figure": {
"type": "method",
"value": None,
"readonly": True,
"doc": None,
"async": False,
"signature": {
"parameters": {
"fig": {"annotation": "Figure", "default": {}},
"format_": {
"annotation": "<class 'str'>",
"default": {
"type": "str",
"value": "png",
"readonly": False,
"doc": None,
},
},
},
"return_annotation": {},
},
"frontend_render": False,
},
"load_from_path": {
"type": "method",
"value": None,
"readonly": True,
"doc": None,
"async": False,
"signature": {
"parameters": {
"path": {
"annotation": "pathlib.Path | str",
"default": {},
}
},
"return_annotation": {},
},
"frontend_render": False,
},
"load_from_url": {
"type": "method",
"value": None,
"readonly": True,
"doc": None,
"async": False,
"signature": {
"parameters": {
"url": {"annotation": "<class 'str'>", "default": {}}
},
"return_annotation": {},
},
"frontend_render": False,
},
"value": {
"type": "str",
"value": "",
"readonly": True,
"doc": None,
},
},
"readonly": False,
"doc": None,
}
},
"readonly": False,
"doc": None,
}

View File

@@ -1,4 +1,5 @@
import asyncio
import enum
from enum import Enum
from typing import Any
@@ -18,6 +19,11 @@ from pydase.utils.serializer import (
)
class MyEnum(enum.Enum):
RUNNING = "running"
FINISHED = "finished"
@pytest.mark.parametrize(
"test_input, expected",
[
@@ -396,33 +402,80 @@ def setup_dict() -> dict[str, Any]:
class ServiceClass(pydase.DataService):
attr1 = 1.0
attr2 = MySubclass()
enum_attr = MyEnum.RUNNING
attr_list = [0, 1, MySubclass()]
def my_task(self) -> None:
pass
return ServiceClass().serialize()["value"]
def test_update_attribute(setup_dict) -> None:
def test_update_attribute(setup_dict: dict[str, Any]) -> None:
set_nested_value_by_path(setup_dict, "attr1", 15)
assert setup_dict["attr1"]["value"] == 15
def test_update_nested_attribute(setup_dict) -> None:
def test_update_nested_attribute(setup_dict: dict[str, Any]) -> None:
set_nested_value_by_path(setup_dict, "attr2.attr3", 25.0)
assert setup_dict["attr2"]["value"]["attr3"]["value"] == 25.0
def test_update_list_entry(setup_dict) -> None:
def test_update_float_attribute_to_enum(setup_dict: dict[str, Any]) -> None:
set_nested_value_by_path(setup_dict, "attr2.attr3", MyEnum.RUNNING)
assert setup_dict["attr2"]["value"]["attr3"] == {
"doc": None,
"enum": {"FINISHED": "finished", "RUNNING": "running"},
"readonly": False,
"type": "Enum",
"value": "RUNNING",
}
def test_update_enum_attribute_to_float(setup_dict: dict[str, Any]) -> None:
set_nested_value_by_path(setup_dict, "enum_attr", 1.01)
assert setup_dict["enum_attr"] == {
"doc": None,
"readonly": False,
"type": "float",
"value": 1.01,
}
def test_update_task_state(setup_dict: dict[str, Any]) -> None:
assert setup_dict["my_task"] == {
"async": False,
"doc": None,
"frontend_render": False,
"readonly": True,
"signature": {"parameters": {}, "return_annotation": {}},
"type": "method",
"value": None,
}
set_nested_value_by_path(setup_dict, "my_task", TaskStatus.RUNNING)
assert setup_dict["my_task"] == {
"async": False,
"doc": None,
"frontend_render": False,
"readonly": True,
"signature": {"parameters": {}, "return_annotation": {}},
"type": "method",
"value": "RUNNING",
}
def test_update_list_entry(setup_dict: dict[str, Any]) -> None:
set_nested_value_by_path(setup_dict, "attr_list[1]", 20)
assert setup_dict["attr_list"]["value"][1]["value"] == 20
def test_update_list_append(setup_dict) -> None:
def test_update_list_append(setup_dict: dict[str, Any]) -> None:
set_nested_value_by_path(setup_dict, "attr_list[3]", 20)
assert setup_dict["attr_list"]["value"][3]["value"] == 20
def test_update_invalid_list_index(
setup_dict, caplog: pytest.LogCaptureFixture
setup_dict: dict[str, Any], caplog: pytest.LogCaptureFixture
) -> None:
set_nested_value_by_path(setup_dict, "attr_list[10]", 30)
assert (