14 Commits

Author SHA1 Message Date
Mose Müller
bcabd2dc48 Merge pull request #95 from tiqi-group/fix/service_configuration
Fix/service configuration
2024-01-29 15:26:27 +01:00
Mose Müller
7ac9c557c2 updates version to v0.5.2 2024-01-29 15:24:13 +01:00
Mose Müller
656529d1fb fixes service configuration (allow all environment variables) 2024-01-29 15:23:27 +01:00
Mose Müller
14601105a7 Merge pull request #93 from tiqi-group/45-placing-the-explanation-question-mark-next-to-the-variable-instead-of-above
feat: placing the explanation question mark next to the variable instead of above
2024-01-16 14:16:38 +01:00
Mose Müller
484b5131e9 fixing enum serialization for python 3.10 2024-01-16 14:13:36 +01:00
Mose Müller
616a5cea21 npm run build 2024-01-16 13:44:37 +01:00
Mose Müller
300bd6ca9a updates Enum serialization 2024-01-16 13:37:39 +01:00
Mose Müller
3e1517e905 udpates dev-guide for adding components 2024-01-16 13:00:01 +01:00
Mose Müller
0ecaeac3fb replaces js interfaces with types 2024-01-16 12:57:35 +01:00
Mose Müller
0e9832e2f1 updates DocStringComponent placement 2024-01-16 12:55:18 +01:00
Mose Müller
0343abd0b0 Merge pull request #91 from tiqi-group/fix/load_from_file
Fix/load from file
2024-01-09 16:39:59 +01:00
Mose Müller
0c149b85b5 updates version to v0.5.1 2024-01-09 16:39:12 +01:00
Mose Müller
0e331e58ff adds tests for server to check if loading from file is working 2024-01-09 16:36:35 +01:00
Mose Müller
45135927e6 initialises observer before loading state from json file 2024-01-09 16:21:57 +01:00
25 changed files with 148 additions and 63 deletions

View File

@@ -118,7 +118,7 @@ import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ImageComponentProps {
type ImageComponentProps = {
name: string;
parentPath?: string;
readOnly: boolean;
@@ -165,14 +165,15 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
>
{displayName} {open ? <ChevronDown /> : <ChevronRight />}
{displayName}
<DocStringComponent docString={docString} />
{open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}
<DocStringComponent docString={docString} />
{/* Your component TSX here */}
</Card.Body>
</Collapse>

View File

@@ -6,7 +6,7 @@ import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
import { WebSettingsContext } from '../WebSettings';
interface AsyncMethodProps {
type AsyncMethodProps = {
name: string;
parentPath: string;
parameters: Record<string, string>;
@@ -14,7 +14,7 @@ interface AsyncMethodProps {
docString?: string;
hideOutput?: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
const { name, parentPath, docString, value: runningTask, addNotification } = props;
@@ -102,14 +102,12 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<h5>
Function: {displayName}
<DocStringComponent docString={docString} />
</h5>
<h5>Function: {displayName}</h5>
<Form onSubmit={execute} ref={formRef}>
{args}
<Button id={`button-${id}`} name={name} value={parentPath} type="submit">
{runningTask ? 'Stop' : 'Start'}
{runningTask ? 'Stop ' : 'Start '}
<DocStringComponent docString={docString} />
</Button>
</Form>
</div>

View File

@@ -6,7 +6,7 @@ import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ButtonComponentProps {
type ButtonComponentProps = {
name: string;
parentPath?: string;
value: boolean;
@@ -14,7 +14,7 @@ interface ButtonComponentProps {
docString: string;
mapping?: [string, string]; // Enforce a tuple of two strings
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
const { name, parentPath, value, readOnly, docString, addNotification } = props;
@@ -48,7 +48,6 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<ToggleButton
id={`toggle-check-${id}`}
type="checkbox"
@@ -58,6 +57,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
disabled={readOnly}
onChange={(e) => setChecked(e.currentTarget.checked)}>
{displayName}
<DocStringComponent docString={docString} />
</ToggleButton>
</div>
);

View File

@@ -6,7 +6,7 @@ import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ColouredEnumComponentProps {
type ColouredEnumComponentProps = {
name: string;
parentPath: string;
value: string;
@@ -14,7 +14,7 @@ interface ColouredEnumComponentProps {
readOnly: boolean;
enumDict: Record<string, string>;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => {
const {
@@ -53,10 +53,12 @@ export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentPro
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<Row>
<Col className="d-flex align-items-center">
<InputGroup.Text>{displayName}</InputGroup.Text>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
{readOnly ? (
// Display the Form.Control when readOnly is true
<Form.Control

View File

@@ -1,9 +1,9 @@
import { Badge, Tooltip, OverlayTrigger } from 'react-bootstrap';
import React from 'react';
interface DocStringProps {
type DocStringProps = {
docString?: string;
}
};
export const DocStringComponent = React.memo((props: DocStringProps) => {
const { docString } = props;

View File

@@ -6,14 +6,14 @@ import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { DocStringComponent } from './DocStringComponent';
import { LevelName } from './NotificationsComponent';
interface EnumComponentProps {
type EnumComponentProps = {
name: string;
parentPath: string;
value: string;
docString?: string;
enumDict: Record<string, string>;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const EnumComponent = React.memo((props: EnumComponentProps) => {
const {
@@ -52,10 +52,12 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<Row>
<Col className="d-flex align-items-center">
<InputGroup.Text>{displayName}</InputGroup.Text>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
<Form.Select
aria-label="Default select example"
value={value}

View File

@@ -27,7 +27,7 @@ type AttributeType =
| 'ColouredEnum';
type ValueType = boolean | string | number | object;
export interface Attribute {
export type Attribute = {
type: AttributeType;
value?: ValueType | ValueType[];
readonly: boolean;
@@ -35,7 +35,7 @@ export interface Attribute {
parameters?: Record<string, string>;
async?: boolean;
enum?: Record<string, string>;
}
};
type GenericComponentProps = {
attribute: Attribute;
name: string;
@@ -95,7 +95,7 @@ export const GenericComponent = React.memo(
<SliderComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
docString={attribute.value['value'].doc}
readOnly={attribute.readonly}
value={attribute.value['value']}
min={attribute.value['min']}
@@ -179,7 +179,7 @@ export const GenericComponent = React.memo(
parentPath={parentPath}
value={attribute.value['value']['value'] as string}
readOnly={attribute.readonly}
docString={attribute.doc}
docString={attribute.value['value'].doc}
// Add any other specific props for the ImageComponent here
format={attribute.value['format']['value'] as string}
addNotification={addNotification}

View File

@@ -6,7 +6,7 @@ import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ImageComponentProps {
type ImageComponentProps = {
name: string;
parentPath: string;
value: string;
@@ -14,7 +14,7 @@ interface ImageComponentProps {
docString: string;
format: string;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const ImageComponent = React.memo((props: ImageComponentProps) => {
const { name, parentPath, value, docString, format, addNotification } = props;
@@ -48,14 +48,15 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
>
{displayName} {open ? <ChevronDown /> : <ChevronRight />}
{displayName}
<DocStringComponent docString={docString} />
{open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}
<DocStringComponent docString={docString} />
{/* Your component JSX here */}
{format === '' && value === '' ? (
<p>No image set in the backend.</p>

View File

@@ -4,14 +4,14 @@ import { Attribute, GenericComponent } from './GenericComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface ListComponentProps {
type ListComponentProps = {
name: string;
parentPath?: string;
value: Attribute[];
docString: string;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const ListComponent = React.memo((props: ListComponentProps) => {
const { name, parentPath, value, docString, isInstantUpdate, addNotification } =

View File

@@ -6,14 +6,14 @@ import { DocStringComponent } from './DocStringComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface MethodProps {
type MethodProps = {
name: string;
parentPath: string;
parameters: Record<string, string>;
docString?: string;
hideOutput?: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const MethodComponent = React.memo((props: MethodProps) => {
const { name, parentPath, docString, addNotification } = props;
@@ -89,12 +89,12 @@ export const MethodComponent = React.memo((props: MethodProps) => {
)}
<h5 onClick={() => setHideOutput(!hideOutput)} style={{ cursor: 'pointer' }}>
Function: {displayName}
<DocStringComponent docString={docString} />
</h5>
<Form onSubmit={execute}>
{args}
<Button variant="primary" type="submit">
Execute
<DocStringComponent docString={docString} />
</Button>
</Form>

View File

@@ -32,7 +32,7 @@ export type FloatObject = {
};
export type NumberObject = IntObject | FloatObject | QuantityObject;
interface NumberComponentProps {
type NumberComponentProps = {
name: string;
type: 'float' | 'int';
parentPath?: string;
@@ -43,7 +43,7 @@ interface NumberComponentProps {
unit?: string;
showName?: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
// TODO: highlight the digit that is being changed by setting both selectionStart and
// selectionEnd
@@ -313,10 +313,14 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<div className="d-flex">
<InputGroup>
{showName && <InputGroup.Text>{displayName}</InputGroup.Text>}
{showName && (
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
)}
<Form.Control
type="text"
value={inputString}

View File

@@ -8,7 +8,7 @@ import { NumberComponent, NumberObject } from './NumberComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface SliderComponentProps {
type SliderComponentProps = {
name: string;
min: NumberObject;
max: NumberObject;
@@ -19,7 +19,7 @@ interface SliderComponentProps {
stepSize: NumberObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const SliderComponent = React.memo((props: SliderComponentProps) => {
const renderCount = useRef(0);
@@ -105,10 +105,12 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<Row>
<Col xs="auto" xl="auto">
<InputGroup.Text>{displayName}</InputGroup.Text>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
</Col>
<Col xs="5" xl>
<Slider

View File

@@ -9,7 +9,7 @@ import { WebSettingsContext } from '../WebSettings';
// TODO: add button functionality
interface StringComponentProps {
type StringComponentProps = {
name: string;
parentPath?: string;
value: string;
@@ -17,7 +17,7 @@ interface StringComponentProps {
docString: string;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
};
export const StringComponent = React.memo((props: StringComponentProps) => {
const { name, parentPath, readOnly, docString, isInstantUpdate, addNotification } =
@@ -70,9 +70,11 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<InputGroup>
<InputGroup.Text>{displayName}</InputGroup.Text>
<InputGroup.Text>
{displayName}
<DocStringComponent docString={docString} />
</InputGroup.Text>
<Form.Control
type="text"
value={inputString}

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydase"
version = "0.5.0"
version = "0.5.2"
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

@@ -15,7 +15,7 @@ class ServiceConfig(BaseConfig): # type: ignore[misc]
web_port: int = 8001
rpc_port: int = 18871
CONFIG_SOURCES = EnvSource(prefix="SERVICE_")
CONFIG_SOURCES = EnvSource(allow_all=True, prefix="SERVICE_")
class WebServerConfig(BaseConfig): # type: ignore[misc]

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.2d8458eb.css",
"main.js": "/static/js/main.ea55bba6.js",
"main.js": "/static/js/main.dba067e7.js",
"index.html": "/index.html",
"main.2d8458eb.css.map": "/static/css/main.2d8458eb.css.map",
"main.ea55bba6.js.map": "/static/js/main.ea55bba6.js.map"
"main.dba067e7.js.map": "/static/js/main.dba067e7.js.map"
},
"entrypoints": [
"static/css/main.2d8458eb.css",
"static/js/main.ea55bba6.js"
"static/js/main.dba067e7.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.ea55bba6.js"></script><link href="/static/css/main.2d8458eb.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.dba067e7.js"></script><link href="/static/css/main.2d8458eb.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

@@ -179,8 +179,8 @@ class Server:
self._state_manager = StateManager(self._service, filename)
if getattr(self._service, "_filename", None) is not None:
self._service._state_manager = self._state_manager
self._state_manager.load_state()
self._observer = DataServiceObserver(self._state_manager)
self._state_manager.load_state()
def run(self) -> None:
"""

View File

@@ -1,5 +1,6 @@
import inspect
import logging
import sys
from collections.abc import Callable
from enum import Enum
from typing import Any
@@ -67,7 +68,9 @@ class Serializer:
def _serialize_enum(obj: Enum) -> dict[str, Any]:
value = obj.name
readonly = False
doc = get_attribute_doc(obj)
doc = obj.__doc__
if sys.version_info < (3, 11) and doc == "An enumeration.":
doc = None
if type(obj).__base__.__name__ == "ColouredEnum":
obj_type = "ColouredEnum"
else:

View File

@@ -1,8 +1,15 @@
import json
import signal
from pytest_mock import MockerFixture
from pathlib import Path
from typing import Any
import pydase
import pydase.components
import pydase.units as u
from pydase.data_service.state_manager import load_state
from pydase.server.server import Server
from pytest import LogCaptureFixture
from pytest_mock import MockerFixture
def test_signal_handling(mocker: MockerFixture):
@@ -33,3 +40,64 @@ def test_signal_handling(mocker: MockerFixture):
# Simulate receiving a SIGINT signal for the second time
server.handle_exit(signal.SIGINT, None)
mock_exit.assert_called_once_with(1)
class Service(pydase.DataService):
def __init__(self, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.some_unit: u.Quantity = 1.2 * u.units.A
self.some_float = 1.0
self._property_attr = 1337.0
@property
def property_attr(self) -> float:
return self._property_attr
@property_attr.setter
@load_state
def property_attr(self, value: float) -> None:
self._property_attr = value
CURRENT_STATE = Service().serialize()
LOAD_STATE = {
"some_float": {
"type": "float",
"value": 10.0,
"readonly": False,
"doc": None,
},
"property_attr": {
"type": "float",
"value": 1337.1,
"readonly": False,
"doc": None,
},
"some_unit": {
"type": "Quantity",
"value": {"magnitude": 12.0, "unit": "A"},
"readonly": False,
"doc": None,
},
}
def test_load_state(tmp_path: Path, caplog: LogCaptureFixture) -> None:
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
# Write a temporary JSON file to read back
with open(file, "w") as f:
json.dump(LOAD_STATE, f, indent=4)
service = Service()
Server(service, filename=str(file))
assert service.some_unit == u.Quantity(12, "A")
assert service.property_attr == 1337.1
assert service.some_float == 10.0
assert "'some_unit' changed to '12.0 A'" in caplog.text
assert "'some_float' changed to '10.0'" in caplog.text
assert "'property_attr' changed to '1337.1'" in caplog.text

View File

@@ -100,6 +100,8 @@ def test_enum_serialize() -> None:
def test_ColouredEnum_serialize() -> None:
class Status(ColouredEnum):
"""Status description."""
PENDING = "#FFA500"
RUNNING = "#0000FF80"
PAUSED = "rgb(169, 169, 169)"
@@ -121,7 +123,7 @@ def test_ColouredEnum_serialize() -> None:
"RUNNING": "#0000FF80",
},
"readonly": False,
"doc": None,
"doc": "Status description.",
}