mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-21 13:41:18 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a76035f443 | ||
|
|
2ab4d1c00a | ||
|
|
a9d577820f | ||
|
|
f5e6dca16a | ||
|
|
4a45d0d438 | ||
|
|
3cc6399f60 | ||
|
|
dc1c7e80f4 |
@@ -23,6 +23,7 @@
|
|||||||
- [Extending with New Components](#extending-with-new-components)
|
- [Extending with New Components](#extending-with-new-components)
|
||||||
- [Customizing Web Interface Style](#customizing-web-interface-style)
|
- [Customizing Web Interface Style](#customizing-web-interface-style)
|
||||||
- [Understanding Service Persistence](#understanding-service-persistence)
|
- [Understanding Service Persistence](#understanding-service-persistence)
|
||||||
|
- [Controlling Property State Loading with `@load_state`](#controlling-property-state-loading-with-load_state)
|
||||||
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
|
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
|
||||||
- [Understanding Units in pydase](#understanding-units-in-pydase)
|
- [Understanding Units in pydase](#understanding-units-in-pydase)
|
||||||
- [Changing the Log Level](#changing-the-log-level)
|
- [Changing the Log Level](#changing-the-log-level)
|
||||||
|
|||||||
@@ -12,14 +12,28 @@ input.instantUpdate {
|
|||||||
}
|
}
|
||||||
.navbarOffset {
|
.navbarOffset {
|
||||||
padding-top: 60px !important;
|
padding-top: 60px !important;
|
||||||
right: 20;
|
|
||||||
}
|
}
|
||||||
/* .toastContainer {
|
.toastContainer {
|
||||||
position: fixed;
|
position: fixed !important;
|
||||||
} */
|
padding: 5px;
|
||||||
|
}
|
||||||
.notificationToast {
|
.notificationToast {
|
||||||
background-color: rgba(114, 214, 253, 0.5) !important;
|
background-color: rgba(114, 214, 253, 0.5) !important;
|
||||||
}
|
}
|
||||||
.exceptionToast {
|
.exceptionToast {
|
||||||
background-color: rgba(216, 41, 18, 0.678) !important;
|
background-color: rgba(216, 41, 18, 0.678) !important;
|
||||||
}
|
}
|
||||||
|
.buttonComponent {
|
||||||
|
float: left !important;
|
||||||
|
margin-right: 10px !important;
|
||||||
|
}
|
||||||
|
.stringComponent {
|
||||||
|
float: left !important;
|
||||||
|
margin-right: 10px !important;
|
||||||
|
}
|
||||||
|
.numberComponent {
|
||||||
|
float: left !important;
|
||||||
|
margin-right: 10px !important;
|
||||||
|
width: 270px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="align-items-center asyncMethodComponent" id={id}>
|
<div className="align-items-center asyncMethodComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<h5>
|
<h5>
|
||||||
Function: {name}
|
Function: {name}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={'buttonComponent'} id={id}>
|
<div className={'buttonComponent'} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
@@ -49,7 +49,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
|
|||||||
value={parentPath}
|
value={parentPath}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
onChange={(e) => setChecked(e.currentTarget.checked)}>
|
onChange={(e) => setChecked(e.currentTarget.checked)}>
|
||||||
<p>{buttonName}</p>
|
{buttonName}
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentPro
|
|||||||
return (
|
return (
|
||||||
<div className={'enumComponent'} id={id}>
|
<div className={'enumComponent'} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const ConnectionToast = React.memo(
|
|||||||
const { message, bg, delay } = getToastContent();
|
const { message, bg, delay } = getToastContent();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContainer position="bottom-center">
|
<ToastContainer position="bottom-center" className="toastContainer">
|
||||||
<Toast
|
<Toast
|
||||||
show={show}
|
show={show}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
|
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'imageComponent'} id={id}>
|
<div className={'imageComponent'} id={id}>
|
||||||
|
{process.env.NODE_ENV === 'development' && (
|
||||||
|
<div>Render count: {renderCount.current}</div>
|
||||||
|
)}
|
||||||
<Card>
|
<Card>
|
||||||
<Card.Header
|
<Card.Header
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(!open)}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={'listComponent'} id={id}>
|
<div className={'listComponent'} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
{value.map((item, index) => {
|
{value.map((item, index) => {
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const MethodComponent = React.memo((props: MethodProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="align-items-center methodComponent" id={id}>
|
<div className="align-items-center methodComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<h5 onClick={() => setHideOutput(!hideOutput)} style={{ cursor: 'pointer' }}>
|
<h5 onClick={() => setHideOutput(!hideOutput)} style={{ cursor: 'pointer' }}>
|
||||||
Function: {name}
|
Function: {name}
|
||||||
@@ -84,11 +84,9 @@ export const MethodComponent = React.memo((props: MethodProps) => {
|
|||||||
</h5>
|
</h5>
|
||||||
<Form onSubmit={execute}>
|
<Form onSubmit={execute}>
|
||||||
{args}
|
{args}
|
||||||
<div>
|
<Button variant="primary" type="submit">
|
||||||
<Button variant="primary" type="submit">
|
Execute
|
||||||
Execute
|
</Button>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
<Collapse in={!hideOutput}>
|
<Collapse in={!hideOutput}>
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ export const Notifications = React.memo((props: NotificationProps) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContainer
|
<ToastContainer className="navbarOffset toastContainer" position="top-end">
|
||||||
className="navbarOffset toastContainer"
|
|
||||||
position="top-end"
|
|
||||||
style={{ position: 'fixed' }}>
|
|
||||||
{showNotification &&
|
{showNotification &&
|
||||||
notifications.map((notification) => (
|
notifications.map((notification) => (
|
||||||
<Toast
|
<Toast
|
||||||
|
|||||||
@@ -289,8 +289,8 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="numberComponent" id={id}>
|
<div className="numberComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && showName && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
<div className="d-flex">
|
<div className="d-flex">
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className="sliderComponent" id={id}>
|
<div className="sliderComponent" id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
return (
|
return (
|
||||||
<div className={'stringComponent'} id={id}>
|
<div className={'stringComponent'} id={id}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<div>Render count: {renderCount.current}</div>
|
||||||
)}
|
)}
|
||||||
<DocStringComponent docString={docString} />
|
<DocStringComponent docString={docString} />
|
||||||
<InputGroup>
|
<InputGroup>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pydase"
|
name = "pydase"
|
||||||
version = "0.3.0"
|
version = "0.3.1"
|
||||||
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."
|
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>"]
|
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ class StateManager:
|
|||||||
value, value_type = nested_json_dict["value"], nested_json_dict["type"]
|
value, value_type = nested_json_dict["value"], nested_json_dict["type"]
|
||||||
class_attr_value_type = nested_class_dict.get("type", None)
|
class_attr_value_type = nested_class_dict.get("type", None)
|
||||||
|
|
||||||
if class_attr_value_type == value_type:
|
if (
|
||||||
|
class_attr_value_type == value_type
|
||||||
|
and self.__is_loadable_state_attribute(path)
|
||||||
|
):
|
||||||
self.set_service_attribute_value_by_path(path, value)
|
self.set_service_attribute_value_by_path(path, value)
|
||||||
else:
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -231,21 +234,36 @@ class StateManager:
|
|||||||
# Traverse the object according to the path parts
|
# Traverse the object according to the path parts
|
||||||
target_obj = get_object_attr_from_path_list(self.service, parent_path_list)
|
target_obj = get_object_attr_from_path_list(self.service, parent_path_list)
|
||||||
|
|
||||||
if self.__attr_value_should_change(target_obj, attr_name):
|
if attr_cache_type in ("ColouredEnum", "Enum"):
|
||||||
if attr_cache_type in ("ColouredEnum", "Enum"):
|
enum_attr = get_object_attr_from_path_list(target_obj, [attr_name])
|
||||||
enum_attr = get_object_attr_from_path_list(target_obj, [attr_name])
|
setattr(target_obj, attr_name, enum_attr.__class__[value])
|
||||||
setattr(target_obj, attr_name, enum_attr.__class__[value])
|
elif attr_cache_type == "list":
|
||||||
elif attr_cache_type == "list":
|
list_obj = get_object_attr_from_path_list(target_obj, [attr_name])
|
||||||
list_obj = get_object_attr_from_path_list(target_obj, [attr_name])
|
list_obj[index] = value
|
||||||
list_obj[index] = value
|
else:
|
||||||
else:
|
setattr(target_obj, attr_name, value)
|
||||||
setattr(target_obj, attr_name, value)
|
|
||||||
|
def __is_loadable_state_attribute(self, property_path: str) -> bool:
|
||||||
|
"""Checks if an attribute defined by a dot-separated path should be loaded from
|
||||||
|
storage.
|
||||||
|
|
||||||
|
For properties, it verifies the presence of the '@load_state' decorator. Regular
|
||||||
|
attributes default to being loadable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parent_object = get_object_attr_from_path_list(
|
||||||
|
self.service, property_path.split(".")[:-1]
|
||||||
|
)
|
||||||
|
attr_name = property_path.split(".")[-1]
|
||||||
|
|
||||||
def __attr_value_should_change(self, parent_object: Any, attr_name: str) -> bool:
|
|
||||||
# If the attribute is a property, change it using the setter without getting
|
|
||||||
# the property value (would otherwise be bad for expensive getter methods)
|
|
||||||
prop = getattr(type(parent_object), attr_name, None)
|
prop = getattr(type(parent_object), attr_name, None)
|
||||||
|
|
||||||
if isinstance(prop, property):
|
if isinstance(prop, property):
|
||||||
return has_load_state_decorator(prop)
|
has_decorator = has_load_state_decorator(prop)
|
||||||
|
if not has_decorator:
|
||||||
|
logger.debug(
|
||||||
|
f"Property {attr_name!r} has no '@load_state' decorator. "
|
||||||
|
"Ignoring value from JSON file..."
|
||||||
|
)
|
||||||
|
return has_decorator
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"files": {
|
"files": {
|
||||||
"main.css": "/static/css/main.c444b055.css",
|
"main.css": "/static/css/main.32559665.css",
|
||||||
"main.js": "/static/js/main.08edc629.js",
|
"main.js": "/static/js/main.6d4f9d3a.js",
|
||||||
"index.html": "/index.html",
|
"index.html": "/index.html",
|
||||||
"main.c444b055.css.map": "/static/css/main.c444b055.css.map",
|
"main.32559665.css.map": "/static/css/main.32559665.css.map",
|
||||||
"main.08edc629.js.map": "/static/js/main.08edc629.js.map"
|
"main.6d4f9d3a.js.map": "/static/js/main.6d4f9d3a.js.map"
|
||||||
},
|
},
|
||||||
"entrypoints": [
|
"entrypoints": [
|
||||||
"static/css/main.c444b055.css",
|
"static/css/main.32559665.css",
|
||||||
"static/js/main.08edc629.js"
|
"static/js/main.6d4f9d3a.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.08edc629.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.6d4f9d3a.js"></script><link href="/static/css/main.32559665.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
1
src/pydase/frontend/static/css/main.32559665.css.map
Normal file
1
src/pydase/frontend/static/css/main.32559665.css.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
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.6d4f9d3a.js.map
Normal file
1
src/pydase/frontend/static/js/main.6d4f9d3a.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -153,7 +153,10 @@ def test_load_state(tmp_path: Path, caplog: LogCaptureFixture):
|
|||||||
assert service.subservice.name == "SubService" # didn't change
|
assert service.subservice.name == "SubService" # didn't change
|
||||||
|
|
||||||
assert "Service.some_unit changed to 12.0 A!" in caplog.text
|
assert "Service.some_unit changed to 12.0 A!" in caplog.text
|
||||||
assert "Attribute 'name' is read-only. Ignoring new value..." in caplog.text
|
assert (
|
||||||
|
"Property 'name' has no '@load_state' decorator. "
|
||||||
|
"Ignoring value from JSON file..." in caplog.text
|
||||||
|
)
|
||||||
assert (
|
assert (
|
||||||
"Attribute type of 'some_float' changed from 'int' to 'float'. "
|
"Attribute type of 'some_float' changed from 'int' to 'float'. "
|
||||||
"Ignoring value from JSON file..."
|
"Ignoring value from JSON file..."
|
||||||
@@ -195,7 +198,11 @@ def test_readonly_attribute(tmp_path: Path, caplog: LogCaptureFixture):
|
|||||||
service = Service()
|
service = Service()
|
||||||
manager = StateManager(service=service, filename=str(file))
|
manager = StateManager(service=service, filename=str(file))
|
||||||
manager.load_state()
|
manager.load_state()
|
||||||
assert "Attribute 'name' is read-only. Ignoring new value..." in caplog.text
|
assert service.name == "Service"
|
||||||
|
assert (
|
||||||
|
"Property 'name' has no '@load_state' decorator. "
|
||||||
|
"Ignoring value from JSON file..." in caplog.text
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_changed_type(tmp_path: Path, caplog: LogCaptureFixture):
|
def test_changed_type(tmp_path: Path, caplog: LogCaptureFixture):
|
||||||
|
|||||||
Reference in New Issue
Block a user