24 Commits

Author SHA1 Message Date
Mose Müller
aca5aab1ef removes unused attribute 2024-02-01 13:25:53 +01:00
Mose Müller
4f1cc4787d Merge pull request #99 from tiqi-group/cleanup/removes_deprecated_code
Cleanup/removes deprecated code
2024-02-01 11:11:43 +01:00
Mose Müller
8efd67d9f3 fixes tests 2024-02-01 10:18:58 +01:00
Mose Müller
34fc0f8739 removes deprecated code 2024-02-01 10:18:49 +01:00
Mose Müller
e60880fd30 Merge pull request #98 from tiqi-group/refactor/passing_full_serialization_dict_to_frontend
Refactor: passing full serialization dict to frontend
2024-02-01 09:27:29 +01:00
Mose Müller
036b0c681a updates version to v0.6.0 (due to breaking changes) 2024-02-01 09:25:47 +01:00
Mose Müller
dd268a4f9b npm run build 2024-02-01 09:18:24 +01:00
Mose Müller
e8638f1f3a fixes tests 2024-02-01 08:45:40 +01:00
Mose Müller
7279fed2aa frontend will can now display any serialization dict 2024-02-01 08:45:40 +01:00
Mose Müller
a2518671da DataService's serialize method now returns whole serialization dict (also passed to frontend) 2024-02-01 08:45:40 +01:00
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
33 changed files with 194 additions and 247 deletions

View File

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

View File

@@ -1,10 +1,6 @@
import { useCallback, useEffect, useReducer, useState } from 'react'; import { useCallback, useEffect, useReducer, useState } from 'react';
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap'; import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
import { hostname, port, socket } from './socket'; import { hostname, port, socket } from './socket';
import {
DataServiceComponent,
DataServiceJSON
} from './components/DataServiceComponent';
import './App.css'; import './App.css';
import { import {
Notifications, Notifications,
@@ -14,6 +10,7 @@ import {
import { ConnectionToast } from './components/ConnectionToast'; import { ConnectionToast } from './components/ConnectionToast';
import { SerializedValue, setNestedValueByPath, State } from './utils/stateUtils'; import { SerializedValue, setNestedValueByPath, State } from './utils/stateUtils';
import { WebSettingsContext, WebSetting } from './WebSettings'; import { WebSettingsContext, WebSetting } from './WebSettings';
import { Attribute, GenericComponent } from './components/GenericComponent';
type Action = type Action =
| { type: 'SET_DATA'; data: State } | { type: 'SET_DATA'; data: State }
@@ -35,7 +32,10 @@ const reducer = (state: State, action: Action): State => {
case 'SET_DATA': case 'SET_DATA':
return action.data; return action.data;
case 'UPDATE_ATTRIBUTE': { case 'UPDATE_ATTRIBUTE': {
return setNestedValueByPath(state, action.fullAccessPath, action.newValue); return {
...state,
value: setNestedValueByPath(state.value, action.fullAccessPath, action.newValue)
};
} }
default: default:
throw new Error(); throw new Error();
@@ -184,9 +184,10 @@ const App = () => {
<div className="App navbarOffset"> <div className="App navbarOffset">
<WebSettingsContext.Provider value={webSettings}> <WebSettingsContext.Provider value={webSettings}>
<DataServiceComponent <GenericComponent
name={''} name=""
props={state as DataServiceJSON} parentPath=""
attribute={state as Attribute}
isInstantUpdate={isInstantUpdate} isInstantUpdate={isInstantUpdate}
addNotification={addNotification} addNotification={addNotification}
/> />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,12 @@ export interface SerializedValue {
async?: boolean; async?: boolean;
parameters?: unknown; parameters?: unknown;
} }
export type State = Record<string, SerializedValue> | null; export type State = {
type: string;
value: Record<string, SerializedValue> | null;
readonly: boolean;
doc: string | null;
};
export function setNestedValueByPath( export function setNestedValueByPath(
serializationDict: Record<string, SerializedValue>, serializationDict: Record<string, SerializedValue>,

View File

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

View File

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

View File

@@ -1,8 +1,7 @@
import inspect import inspect
import logging import logging
import warnings
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING, Any, get_type_hints from typing import Any, get_type_hints
import rpyc # type: ignore[import-untyped] import rpyc # type: ignore[import-untyped]
@@ -15,20 +14,12 @@ from pydase.observer_pattern.observable.observable import (
from pydase.utils.helpers import ( from pydase.utils.helpers import (
convert_arguments_to_hinted_types, convert_arguments_to_hinted_types,
get_class_and_instance_attributes, get_class_and_instance_attributes,
get_object_attr_from_path_list,
is_property_attribute, is_property_attribute,
parse_list_attr_and_index,
update_value_if_changed,
) )
from pydase.utils.serializer import ( from pydase.utils.serializer import (
Serializer, Serializer,
generate_serialized_data_paths,
get_nested_dict_by_path,
) )
if TYPE_CHECKING:
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,18 +42,7 @@ class DataService(rpyc.Service, AbstractDataService):
if not hasattr(self, "_autostart_tasks"): if not hasattr(self, "_autostart_tasks"):
self._autostart_tasks = {} self._autostart_tasks = {}
filename = kwargs.pop("filename", None)
if filename is not None:
warnings.warn(
"The 'filename' argument is deprecated and will be removed in a future "
"version. Please pass the 'filename' argument to `pydase.Server`.",
DeprecationWarning,
stacklevel=2,
)
self._filename: str | Path = filename
self.__check_instance_classes() self.__check_instance_classes()
self._initialised = True
def __setattr__(self, __name: str, __value: Any) -> None: def __setattr__(self, __name: str, __value: Any) -> None:
# Check and warn for unexpected type changes in attributes # Check and warn for unexpected type changes in attributes
@@ -125,27 +105,6 @@ class DataService(rpyc.Service, AbstractDataService):
): ):
self.__warn_if_not_observable(attr_value) self.__warn_if_not_observable(attr_value)
def __set_attribute_based_on_type( # noqa: PLR0913
self,
target_obj: Any,
attr_name: str,
attr: Any,
value: Any,
index: int | None,
path_list: list[str],
) -> None:
if isinstance(attr, Enum):
update_value_if_changed(target_obj, attr_name, attr.__class__[value])
elif isinstance(attr, list) and index is not None:
update_value_if_changed(attr, index, value)
elif isinstance(attr, DataService) and isinstance(value, dict):
for key, v in value.items():
self.update_DataService_attribute([*path_list, attr_name], key, v)
elif callable(attr):
process_callable_attribute(attr, value["args"])
else:
update_value_if_changed(target_obj, attr_name, value)
def _rpyc_getattr(self, name: str) -> Any: def _rpyc_getattr(self, name: str) -> Any:
if name.startswith("_"): if name.startswith("_"):
# disallow special and private attributes # disallow special and private attributes
@@ -166,71 +125,6 @@ class DataService(rpyc.Service, AbstractDataService):
# allow all other attributes # allow all other attributes
setattr(self, name, value) setattr(self, name, value)
def write_to_file(self) -> None:
"""
Serialize the DataService instance and write it to a JSON file.
This method is deprecated and will be removed in a future version.
Service persistence is handled by `pydase.Server` now, instead.
"""
warnings.warn(
"'write_to_file' is deprecated and will be removed in a future version. "
"Service persistence is handled by `pydase.Server` now, instead.",
DeprecationWarning,
stacklevel=2,
)
if hasattr(self, "_state_manager"):
self._state_manager.save_state()
def load_DataService_from_JSON( # noqa: N802
self, json_dict: dict[str, Any]
) -> None:
warnings.warn(
"'load_DataService_from_JSON' is deprecated and will be removed in a "
"future version. "
"Service persistence is handled by `pydase.Server` now, instead.",
DeprecationWarning,
stacklevel=2,
)
# Traverse the serialized representation and set the attributes of the class
serialized_class = self.serialize()
for path in generate_serialized_data_paths(json_dict):
nested_json_dict = get_nested_dict_by_path(json_dict, path)
value = nested_json_dict["value"]
value_type = nested_json_dict["type"]
nested_class_dict = get_nested_dict_by_path(serialized_class, path)
class_value_type = nested_class_dict.get("type", None)
if class_value_type == value_type:
class_attr_is_read_only = nested_class_dict["readonly"]
if class_attr_is_read_only:
logger.debug(
"Attribute '%s' is read-only. Ignoring value from JSON "
"file...",
path,
)
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.update_DataService_attribute(parts[:-1], attr_name, value)
else:
logger.info(
"Attribute type of '%s' changed from '%s' to "
"'%s'. Ignoring value from JSON file...",
path,
value_type,
class_value_type,
)
def serialize(self) -> dict[str, dict[str, Any]]: def serialize(self) -> dict[str, dict[str, Any]]:
""" """
Serializes the instance into a dictionary, preserving the structure of the Serializes the instance into a dictionary, preserving the structure of the
@@ -248,38 +142,4 @@ class DataService(rpyc.Service, AbstractDataService):
Returns: Returns:
dict: The serialized instance. dict: The serialized instance.
""" """
return Serializer.serialize_object(self)["value"] return Serializer.serialize_object(self)
def update_DataService_attribute( # noqa: N802
self,
path_list: list[str],
attr_name: str,
value: Any,
) -> None:
warnings.warn(
"'update_DataService_attribute' is deprecated and will be removed in a "
"future version. "
"Service state management is handled by `pydase.data_service.state_manager`"
"now, instead.",
DeprecationWarning,
stacklevel=2,
)
# If attr_name corresponds to a list entry, extract the attr_name and the index
attr_name, index = parse_list_attr_and_index(attr_name)
# Traverse the object according to the path parts
target_obj = get_object_attr_from_path_list(self, path_list)
# If the attribute is a property, change it using the setter without getting the
# property value (would otherwise be bad for expensive getter methods)
if is_property_attribute(target_obj, attr_name):
setattr(target_obj, attr_name, value)
return
attr = get_object_attr_from_path_list(target_obj, [attr_name])
if attr is None:
return
self.__set_attribute_based_on_type(
target_obj, attr_name, attr, value, index, path_list
)

View File

@@ -30,10 +30,10 @@ class DataServiceCache:
self._cache = self.service.serialize() self._cache = self.service.serialize()
def update_cache(self, full_access_path: str, value: Any) -> None: def update_cache(self, full_access_path: str, value: Any) -> None:
set_nested_value_by_path(self._cache, full_access_path, value) set_nested_value_by_path(self._cache["value"], full_access_path, value)
def get_value_dict_from_cache(self, full_access_path: str) -> dict[str, Any]: def get_value_dict_from_cache(self, full_access_path: str) -> dict[str, Any]:
try: try:
return get_nested_dict_by_path(self._cache, full_access_path) return get_nested_dict_by_path(self._cache["value"], full_access_path)
except (SerializationPathError, SerializationValueError, KeyError): except (SerializationPathError, SerializationValueError, KeyError):
return {} return {}

View File

@@ -126,7 +126,7 @@ class StateManager:
if self.filename is not None: if self.filename is not None:
with open(self.filename, "w") as f: with open(self.filename, "w") as f:
json.dump(self.cache, f, indent=4) json.dump(self.cache["value"], f, indent=4)
else: else:
logger.info( logger.info(
"State manager was not initialised with a filename. Skipping " "State manager was not initialised with a filename. Skipping "
@@ -191,7 +191,7 @@ class StateManager:
value: The new value to set for the attribute. value: The new value to set for the attribute.
""" """
current_value_dict = get_nested_dict_by_path(self.cache, path) current_value_dict = get_nested_dict_by_path(self.cache["value"], path)
# This will also filter out methods as they are 'read-only' # This will also filter out methods as they are 'read-only'
if current_value_dict["readonly"]: if current_value_dict["readonly"]:
@@ -234,7 +234,7 @@ class StateManager:
# Update path to reflect the attribute without list indices # Update path to reflect the attribute without list indices
path = ".".join([*parent_path_list, attr_name]) path = ".".join([*parent_path_list, attr_name])
attr_cache_type = get_nested_dict_by_path(self.cache, path)["type"] attr_cache_type = get_nested_dict_by_path(self.cache["value"], path)["type"]
# 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)
@@ -273,7 +273,7 @@ class StateManager:
return has_decorator return has_decorator
cached_serialization_dict = get_nested_dict_by_path( cached_serialization_dict = get_nested_dict_by_path(
self.cache, full_access_path self.cache["value"], full_access_path
) )
if cached_serialization_dict["value"] == "method": if cached_serialization_dict["value"] == "method":

View File

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

@@ -177,10 +177,8 @@ class Server:
self.servers: dict[str, asyncio.Future[Any]] = {} self.servers: dict[str, asyncio.Future[Any]] = {}
self.executor: ThreadPoolExecutor | None = None self.executor: ThreadPoolExecutor | None = None
self._state_manager = StateManager(self._service, filename) 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._observer = DataServiceObserver(self._state_manager)
self._state_manager.load_state()
def run(self) -> None: def run(self) -> None:
""" """

View File

@@ -126,7 +126,7 @@ class WebServer:
@property @property
def web_settings(self) -> dict[str, dict[str, Any]]: def web_settings(self) -> dict[str, dict[str, Any]]:
current_web_settings = self._get_web_settings_from_file() current_web_settings = self._get_web_settings_from_file()
for path in generate_serialized_data_paths(self.state_manager.cache): for path in generate_serialized_data_paths(self.state_manager.cache["value"]):
if path in current_web_settings: if path in current_web_settings:
continue continue

View File

@@ -1,5 +1,6 @@
import inspect import inspect
import logging import logging
import sys
from collections.abc import Callable from collections.abc import Callable
from enum import Enum from enum import Enum
from typing import Any from typing import Any
@@ -67,7 +68,9 @@ class Serializer:
def _serialize_enum(obj: Enum) -> dict[str, Any]: def _serialize_enum(obj: Enum) -> dict[str, Any]:
value = obj.name value = obj.name
readonly = False 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": if type(obj).__base__.__name__ == "ColouredEnum":
obj_type = "ColouredEnum" obj_type = "ColouredEnum"
else: else:

View File

@@ -5,7 +5,6 @@ from typing import Any
import pydase import pydase
import pydase.components import pydase.components
import pydase.units as u import pydase.units as u
import pytest
from pydase.data_service.data_service_observer import DataServiceObserver from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import ( from pydase.data_service.state_manager import (
StateManager, StateManager,
@@ -91,7 +90,7 @@ class Service(pydase.DataService):
self._property_attr = value self._property_attr = value
CURRENT_STATE = Service().serialize() CURRENT_STATE = Service().serialize()["value"]
LOAD_STATE = { LOAD_STATE = {
"list_attr": { "list_attr": {
@@ -251,16 +250,6 @@ def test_load_state(tmp_path: Path, caplog: LogCaptureFixture) -> None:
assert "'my_slider.step_size' changed to '2.0'" in caplog.text assert "'my_slider.step_size' changed to '2.0'" in caplog.text
def test_filename_warning(tmp_path: Path, caplog: LogCaptureFixture) -> None:
file = tmp_path / "test_state.json"
with pytest.warns(DeprecationWarning):
service = Service(filename=str(file))
StateManager(service=service, filename=str(file))
assert f"Overwriting filename {str(file)!r} with {str(file)!r}." in caplog.text
def test_filename_error(caplog: LogCaptureFixture) -> None: def test_filename_error(caplog: LogCaptureFixture) -> None:
service = Service() service = Service()
manager = StateManager(service=service) manager = StateManager(service=service)

View File

@@ -1,8 +1,15 @@
import json
import signal import signal
from pathlib import Path
from pytest_mock import MockerFixture from typing import Any
import pydase 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): 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 # Simulate receiving a SIGINT signal for the second time
server.handle_exit(signal.SIGINT, None) server.handle_exit(signal.SIGINT, None)
mock_exit.assert_called_once_with(1) 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

@@ -1,9 +1,10 @@
from typing import Any from typing import Any
import pydase
import pydase.units as u import pydase.units as u
from pydase.data_service.data_service import DataService from pydase.data_service.data_service import DataService
from pydase.data_service.data_service_observer import DataServiceObserver from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager from pydase.data_service.state_manager import StateManager, load_state
from pytest import LogCaptureFixture from pytest import LogCaptureFixture
@@ -99,7 +100,10 @@ def test_autoconvert_offset_to_baseunit() -> None:
def test_loading_from_json(caplog: LogCaptureFixture) -> None: def test_loading_from_json(caplog: LogCaptureFixture) -> None:
"""This function tests if the quantity read from the json description is actually """This function tests if the quantity read from the json description is actually
passed as a quantity to the property setter.""" passed as a quantity to the property setter."""
JSON_DICT = { import json
import tempfile
serialization_dict = {
"some_unit": { "some_unit": {
"type": "Quantity", "type": "Quantity",
"value": {"magnitude": 10.0, "unit": "A"}, "value": {"magnitude": 10.0, "unit": "A"},
@@ -118,14 +122,17 @@ def test_loading_from_json(caplog: LogCaptureFixture) -> None:
return self._unit return self._unit
@some_unit.setter @some_unit.setter
@load_state
def some_unit(self, value: u.Quantity) -> None: def some_unit(self, value: u.Quantity) -> None:
assert isinstance(value, u.Quantity) assert isinstance(value, u.Quantity)
self._unit = value self._unit = value
service_instance = ServiceClass() service_instance = ServiceClass()
state_manager = StateManager(service_instance)
DataServiceObserver(state_manager)
service_instance.load_DataService_from_JSON(JSON_DICT) fp = tempfile.NamedTemporaryFile("w+")
json.dump(serialization_dict, fp)
fp.seek(0)
pydase.Server(service_instance, filename=fp.name)
assert "'some_unit' changed to '10.0 A'" in caplog.text assert "'some_unit' changed to '10.0 A'" in caplog.text

View File

@@ -100,6 +100,8 @@ def test_enum_serialize() -> None:
def test_ColouredEnum_serialize() -> None: def test_ColouredEnum_serialize() -> None:
class Status(ColouredEnum): class Status(ColouredEnum):
"""Status description."""
PENDING = "#FFA500" PENDING = "#FFA500"
RUNNING = "#0000FF80" RUNNING = "#0000FF80"
PAUSED = "rgb(169, 169, 169)" PAUSED = "rgb(169, 169, 169)"
@@ -121,7 +123,7 @@ def test_ColouredEnum_serialize() -> None:
"RUNNING": "#0000FF80", "RUNNING": "#0000FF80",
}, },
"readonly": False, "readonly": False,
"doc": None, "doc": "Status description.",
} }
@@ -321,7 +323,7 @@ def test_derived_data_service_serialization() -> None:
@pytest.fixture @pytest.fixture
def setup_dict(): def setup_dict() -> dict[str, Any]:
class MySubclass(pydase.DataService): class MySubclass(pydase.DataService):
attr3 = 1.0 attr3 = 1.0
list_attr = [1.0, 1] list_attr = [1.0, 1]
@@ -331,30 +333,32 @@ def setup_dict():
attr2 = MySubclass() attr2 = MySubclass()
attr_list = [0, 1, MySubclass()] attr_list = [0, 1, MySubclass()]
return ServiceClass().serialize() return ServiceClass().serialize()["value"]
def test_update_attribute(setup_dict): def test_update_attribute(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr1", 15) set_nested_value_by_path(setup_dict, "attr1", 15)
assert setup_dict["attr1"]["value"] == 15 assert setup_dict["attr1"]["value"] == 15
def test_update_nested_attribute(setup_dict): def test_update_nested_attribute(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr2.attr3", 25.0) set_nested_value_by_path(setup_dict, "attr2.attr3", 25.0)
assert setup_dict["attr2"]["value"]["attr3"]["value"] == 25.0 assert setup_dict["attr2"]["value"]["attr3"]["value"] == 25.0
def test_update_list_entry(setup_dict): def test_update_list_entry(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr_list[1]", 20) set_nested_value_by_path(setup_dict, "attr_list[1]", 20)
assert setup_dict["attr_list"]["value"][1]["value"] == 20 assert setup_dict["attr_list"]["value"][1]["value"] == 20
def test_update_list_append(setup_dict): def test_update_list_append(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr_list[3]", 20) set_nested_value_by_path(setup_dict, "attr_list[3]", 20)
assert setup_dict["attr_list"]["value"][3]["value"] == 20 assert setup_dict["attr_list"]["value"][3]["value"] == 20
def test_update_invalid_list_index(setup_dict, caplog: pytest.LogCaptureFixture): def test_update_invalid_list_index(
setup_dict, caplog: pytest.LogCaptureFixture
) -> None:
set_nested_value_by_path(setup_dict, "attr_list[10]", 30) set_nested_value_by_path(setup_dict, "attr_list[10]", 30)
assert ( assert (
"Error occured trying to change 'attr_list[10]': list index " "Error occured trying to change 'attr_list[10]': list index "