mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-20 05:01:19 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aca5aab1ef | ||
|
|
4f1cc4787d | ||
|
|
8efd67d9f3 | ||
|
|
34fc0f8739 | ||
|
|
e60880fd30 | ||
|
|
036b0c681a | ||
|
|
dd268a4f9b | ||
|
|
e8638f1f3a | ||
|
|
7279fed2aa | ||
|
|
a2518671da | ||
|
|
bcabd2dc48 | ||
|
|
7ac9c557c2 | ||
|
|
656529d1fb | ||
|
|
14601105a7 | ||
|
|
484b5131e9 | ||
|
|
616a5cea21 | ||
|
|
300bd6ca9a | ||
|
|
3e1517e905 | ||
|
|
0ecaeac3fb | ||
|
|
0e9832e2f1 | ||
|
|
0343abd0b0 | ||
|
|
0c149b85b5 | ||
|
|
0e331e58ff | ||
|
|
45135927e6 |
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 } =
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -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
1
src/pydase/frontend/static/js/main.1b1d7066.js.map
Normal file
1
src/pydase/frontend/static/js/main.1b1d7066.js.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
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 "
|
||||||
|
|||||||
Reference in New Issue
Block a user