12 Commits

Author SHA1 Message Date
Mose Müller
aa85f6453f update version to v0.2.0 2023-10-11 14:20:35 +02:00
Mose Müller
343354e0ee Update README.md 2023-10-11 14:17:40 +02:00
Mose Müller
b38bb05c69 Merge pull request #28 from tiqi-group/5-adding-status-component
5 adding coloured enum component
2023-10-11 14:15:34 +02:00
Mose Müller
a0dab630f9 Update README.md 2023-10-11 14:03:07 +02:00
Mose Müller
a9db7848f7 fix: pytest failed after moving from StrEnum to Enum 2023-10-11 14:03:07 +02:00
Mose Müller
a8b14180ad fix: using Enum instead of StrEnum (>=3.11 only) 2023-10-11 14:03:07 +02:00
Mose Müller
26a366842a frontend: npm run build 2023-10-11 14:03:07 +02:00
Mose Müller
b0e7de2d2c docs: updating Readme 2023-10-11 14:03:07 +02:00
Mose Müller
bbcba8b39f test: adding test for ColouredEnum component 2023-10-11 14:03:07 +02:00
Mose Müller
34e46e05ee feat: adding ColouredEnum component 2023-10-11 14:03:07 +02:00
Mose Müller
93c2f5ab70 docs: updating mkdocs documentation
- adding user guide section
- removing "baselevel: 4"
2023-10-11 13:58:55 +02:00
Mose Müller
106ffbfc40 removing .python-version 2023-10-11 13:52:13 +02:00
19 changed files with 322 additions and 50 deletions

View File

125
README.md
View File

@@ -129,7 +129,7 @@ if __name__ == "__main__":
Server(service).run()
```
This will start the server, making your Device service accessible via RPC and a web server at http://localhost:8001.
This will start the server, making your Device service accessible via RPC and a web server at [http://localhost:8001](http://localhost:8001).
### Accessing the Web Interface
@@ -159,11 +159,15 @@ In this example, replace `<ip_addr>` with the IP address of the machine where th
<!--usage-end-->
## Understanding the Component System
<!-- Component User Guide Start -->
In `pydase`, components are fundamental building blocks that bridge the Python backend logic with frontend visual representation and interactions. This system can be understood based on the following categories:
### Built-in Type and Enum Components
`pydase` automatically maps standard Python data types to their corresponding frontend components:
- `str`: Translated into a `StringComponent` on the frontend.
- `int` and `float`: Manifested as the `NumberComponent`.
- `bool`: Rendered as a `ButtonComponent`.
@@ -173,6 +177,7 @@ In `pydase`, components are fundamental building blocks that bridge the Python b
### Method Components
Methods within the `DataService` class have frontend representations:
- Regular Methods: These are rendered as a `MethodComponent` in the frontend, allowing users to execute the method via an "execute" button.
- Asynchronous Methods: These are manifested as the `AsyncMethodComponent` with "start"/"stop" buttons to manage the execution of [tasks](#understanding-tasks-in-pydase).
@@ -228,62 +233,112 @@ The custom components in `pydase` have two main parts:
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
- `Image`: This component allows users to display and update images within the application.
#### `Image`
```python
import matplotlib.pyplot as plt
import numpy as np
This component provides a versatile interface for displaying images within the application. Users can update and manage images from various sources, including local paths, URLs, and even matplotlib figures.
import pydase
from pydase.components.image import Image
The component offers methods to load images seamlessly, ensuring that visual content is easily integrated and displayed within the data service.
```python
import matplotlib.pyplot as plt
import numpy as np
import pydase
from pydase.components.image import Image
class MyDataService(pydase.DataService):
my_image = Image()
class MyDataService(pydase.DataService):
my_image = Image()
if __name__ == "__main__":
service = MyDataService()
# loading from local path
service.my_image.load_from_path("/your/image/path/")
if __name__ == "__main__":
service = MyDataService()
# loading from local path
service.my_image.load_from_path("/your/image/path/")
# loading from a URL
service.my_image.load_from_url("https://cataas.com/cat")
# loading from a URL
service.my_image.load_from_url("https://cataas.com/cat")
# loading a matplotlib figure
fig = plt.figure()
x = np.linspace(0, 2 * np.pi)
plt.plot(x, np.sin(x))
plt.grid()
service.my_image.load_from_matplotlib_figure(fig)
# loading a matplotlib figure
fig = plt.figure()
x = np.linspace(0, 2 * np.pi)
plt.plot(x, np.sin(x))
plt.grid()
service.my_image.load_from_matplotlib_figure(fig)
pydase.Server(service).run()
```
pydase.Server(service).run()
```
![Image Component](docs/images/Image_component.png)
![Image Component](docs/images/Image_component.png)
- `NumberSlider`: An interactive slider component to adjust numerical values, including floats and integers, on the frontend while synchronizing the data with the backend in real-time.
#### `NumberSlider`
```python
import pydase
from pydase.components import NumberSlider
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
```python
import pydase
from pydase.components import NumberSlider
class MyService(pydase.DataService):
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1)
class MyService(pydase.DataService):
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float")
if __name__ == "__main__":
service = MyService()
pydase.Server(service).run()
```
if __name__ == "__main__":
service = MyService()
pydase.Server(service).run()
```
![Slider Component](docs/images/Slider_component.png)
![Slider Component](docs/images/Slider_component.png)
#### `ColouredEnum`
This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more.
If the property associated with the `ColouredEnum` has a setter function, the keys of the enum will be rendered as a dropdown menu, allowing users to interact and select different options. Without a setter function, the selected key will simply be displayed as a coloured box with text inside, serving as a visual indicator.
```python
import pydase
import pydase.components as pyc
class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Hexadecimal colour (Orange)
RUNNING = "#0000FF80" # Hexadecimal colour with transparency (Blue)
PAUSED = "rgb(169, 169, 169)" # RGB colour (Dark Gray)
RETRYING = "rgba(255, 255, 0, 0.3)" # RGB colour with transparency (Yellow)
COMPLETED = "hsl(120, 100%, 50%)" # HSL colour (Green)
FAILED = "hsla(0, 100%, 50%, 0.7)" # HSL colour with transparency (Red)
CANCELLED = "SlateGray" # Cross-browser colour name (Slate Gray)
class StatusTest(pydase.DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# do something ...
self._status = value
# Modifying or accessing the status value:
my_service = StatusExample()
my_service.status = MyStatus.FAILED
```
![ColouredEnum Component](docs/images/ColouredEnum_component.png)
#### Extending with New Components
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](https://pydase.readthedocs.io/en/latest/dev-guide/Adding_Components/) provides detailed steps on achieving this.
<!-- Component User Guide End -->
## Understanding Service Persistence
`pydase` allows you to easily persist the state of your service by saving it to a file. This is especially useful when you want to maintain the service's state across different runs.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,7 @@
# Components Guide
{%
include-markdown "../../README.md"
start="<!-- Component User Guide Start -->"
end="<!-- Component User Guide End -->"
heading-offset=-2
%}

View File

@@ -0,0 +1,75 @@
import React, { useEffect, useRef } from 'react';
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { emit_update } from '../socket';
import { DocStringComponent } from './DocStringComponent';
interface ColouredEnumComponentProps {
name: string;
parentPath: string;
value: string;
docString?: string;
readOnly: boolean;
enumDict: Record<string, string>;
addNotification: (string) => void;
}
export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => {
const {
name,
parentPath: parentPath,
value,
docString,
enumDict,
readOnly,
addNotification
} = props;
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
});
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
}, [props.value]);
const handleValueChange = (newValue) => {
console.log(newValue);
emit_update(name, parentPath, newValue);
};
return (
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}
<DocStringComponent docString={docString} />
<Row>
<Col className="d-flex align-items-center">
<InputGroup.Text>{name}</InputGroup.Text>
{readOnly ? (
// Display the Form.Control when readOnly is true
<Form.Control
value={value}
disabled={true}
style={{ backgroundColor: enumDict[value] }}
/>
) : (
// Display the Form.Select when readOnly is false
<Form.Select
aria-label="coloured-enum-select"
value={value}
style={{ backgroundColor: enumDict[value] }}
onChange={(event) => handleValueChange(event.target.value)}>
{Object.entries(enumDict).map(([key, val]) => (
<option key={key} value={key}>
{key}
</option>
))}
</Form.Select>
)}
</Col>
</Row>
</div>
);
});

View File

@@ -9,6 +9,7 @@ import { StringComponent } from './StringComponent';
import { ListComponent } from './ListComponent';
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
import { ImageComponent } from './ImageComponent';
import { ColouredEnumComponent } from './ColouredEnumComponent';
type AttributeType =
| 'str'
@@ -21,7 +22,8 @@ type AttributeType =
| 'DataService'
| 'Enum'
| 'NumberSlider'
| 'Image';
| 'Image'
| 'ColouredEnum';
type ValueType = boolean | string | number | object;
export interface Attribute {
@@ -181,6 +183,19 @@ export const GenericComponent = React.memo(
addNotification={addNotification}
/>
);
} else if (attribute.type === 'ColouredEnum') {
console.log(attribute);
return (
<ColouredEnumComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
value={String(attribute.value)}
readOnly={attribute.readonly}
enumDict={attribute.enum}
addNotification={addNotification}
/>
);
} else {
return <div key={name}>{name}</div>;
}

View File

@@ -4,6 +4,8 @@ edit_uri: blob/docs/docs/
nav:
- Home: index.md
- Getting Started: getting-started.md
- User Guide:
- Components Guide: user-guide/Components.md
- Developer Guide:
- Developer Guide: dev-guide/README.md
- API Reference: dev-guide/api.md
@@ -22,7 +24,6 @@ markdown_extensions:
- smarty
- toc:
permalink: true
baselevel: 4
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.snippets
@@ -38,5 +39,3 @@ plugins:
watch:
- src/pydase

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydase"
version = "0.1.2"
version = "0.2.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."
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
readme = "README.md"

View File

@@ -27,10 +27,12 @@ print(my_service.voltage.value) # Output: 5
```
"""
from pydase.components.coloured_enum import ColouredEnum
from pydase.components.image import Image
from pydase.components.number_slider import NumberSlider
__all__ = [
"NumberSlider",
"Image",
"ColouredEnum",
]

View File

@@ -0,0 +1,61 @@
from enum import Enum
class ColouredEnum(Enum):
"""
Represents a UI element that can display colour-coded text based on its value.
This class extends the standard Enum but requires its values to be valid CSS
colour codes. Supported colour formats include:
- Hexadecimal colours
- Hexadecimal colours with transparency
- RGB colours
- RGBA colours
- HSL colours
- HSLA colours
- Predefined/Cross-browser colour names
Refer to the this website for more details on colour formats:
(https://www.w3schools.com/cssref/css_colours_legal.php)
The behavior of this component in the UI depends on how it's defined in the data
service:
- As property with a setter or as attribute: Renders as a dropdown menu,
allowing users to select and change its value from the frontend.
- As property without a setter: Displays as a coloured box with the key of the
`ColouredEnum` as text inside, serving as a visual indicator without user
interaction.
Example:
--------
```python
import pydase.components as pyc
import pydase
class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Orange
RUNNING = "#0000FF80" # Transparent Blue
PAUSED = "rgb(169, 169, 169)" # Dark Gray
RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow
COMPLETED = "hsl(120, 100%, 50%)" # Green
FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red
CANCELLED = "SlateGray" # Slate Gray
class StatusExample(pydase.DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# Custom logic here...
self._status = value
# Example usage:
my_service = StatusExample()
my_service.status = MyStatus.FAILED
```
"""
pass

View File

@@ -311,8 +311,12 @@ class DataService(rpyc.Service, AbstractDataService):
"value": running_task_info,
}
elif isinstance(value, Enum):
if type(value).__base__.__name__ == "ColouredEnum":
val_type = "ColouredEnum"
else:
val_type = "Enum"
result[key] = {
"type": "Enum",
"type": val_type,
"value": value.name,
"enum": {
name: member.value

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.398bc7f8.css",
"main.js": "/static/js/main.c348625e.js",
"main.js": "/static/js/main.553ca0c9.js",
"index.html": "/index.html",
"main.398bc7f8.css.map": "/static/css/main.398bc7f8.css.map",
"main.c348625e.js.map": "/static/js/main.c348625e.js.map"
"main.553ca0c9.js.map": "/static/js/main.553ca0c9.js.map"
},
"entrypoints": [
"static/css/main.398bc7f8.css",
"static/js/main.c348625e.js"
"static/js/main.553ca0c9.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.c348625e.js"></script><link href="/static/css/main.398bc7f8.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.553ca0c9.js"></script><link href="/static/css/main.398bc7f8.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

@@ -1,5 +1,7 @@
from loguru import logger
from pydase.utils.helpers import get_component_class_names
def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> None:
base_class_name = __value.__class__.__base__.__name__
@@ -13,7 +15,8 @@ def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) ->
"asyncio.unix_events",
"_abc",
]
and base_class_name not in ["DataService", "list", "Enum"]
and base_class_name
not in ["DataService", "list", "Enum"] + get_component_class_names()
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
):
logger.warning(

View File

@@ -0,0 +1,51 @@
from pytest import CaptureFixture, LogCaptureFixture
from pydase.components.coloured_enum import ColouredEnum
from pydase.data_service.data_service import DataService
from .. import caplog # noqa
def test_ColouredEnum(capsys: CaptureFixture) -> None:
class MyStatus(ColouredEnum):
RUNNING = "#00FF00"
FAILING = "#FF0000"
class ServiceClass(DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# do something ...
self._status = value
service = ServiceClass()
service.status = MyStatus.FAILING
captured = capsys.readouterr()
expected_output = sorted(
[
"ServiceClass.status = MyStatus.FAILING",
]
)
actual_output = sorted(captured.out.strip().split("\n")) # type: ignore
assert actual_output == expected_output
def test_warning(caplog: LogCaptureFixture) -> None: # noqa
class MyStatus(ColouredEnum):
RUNNING = "#00FF00"
FAILING = "#FF0000"
class ServiceClass(DataService):
status = MyStatus.RUNNING
assert (
"Warning: Class MyStatus does not inherit from DataService." not in caplog.text
)