mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-12 15:57:12 +02:00
updates Readme (moving components guide to docs, removing TOC, updated features list,...)
This commit is contained in:
508
README.md
508
README.md
@ -5,54 +5,19 @@
|
|||||||
[](https://pydase.readthedocs.io/en/latest/?badge=stable)
|
[](https://pydase.readthedocs.io/en/latest/?badge=stable)
|
||||||
[](https://github.com/tiqi-group/pydase/blob/main/LICENSE)
|
[](https://github.com/tiqi-group/pydase/blob/main/LICENSE)
|
||||||
|
|
||||||
`pydase` is a Python library that simplifies the creation of remote control interfaces for Python objects. It exposes the public attributes of a user-defined class via a Socket.IO web server. Users can interact with these attributes using an RPC client, a RESTful API, or a web browser. The web browser frontend is auto-generated, displaying components that correspond to each public attribute of the class for direct interaction. `pydase` leverages the observer pattern and Socket.IO to provide real-time updates, ensuring that changes to the class attributes are reflected across all clients.
|
`pydase` is a Python library that simplifies the creation of remote control interfaces for Python objects. It exposes the public attributes of a user-defined class via a Socket.IO web server, ensuring they are always in sync with the service state. You can interact with these attributes using an RPC client, a RESTful API, or a web browser. The web browser frontend is auto-generated, displaying components that correspond to each public attribute of the class for direct interaction.
|
||||||
|
`pydase` implements an observer pattern and to provide real-time updates, ensuring that changes to the class attributes are reflected across all clients.
|
||||||
|
|
||||||
Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates service development and deployment.
|
Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates service development and deployment.
|
||||||
|
|
||||||
- [Features](#features)
|
|
||||||
- [Installation](#installation)
|
|
||||||
- [Usage](#usage)
|
|
||||||
- [Defining a DataService](#defining-a-dataservice)
|
|
||||||
- [Running the Server](#running-the-server)
|
|
||||||
- [Accessing the Web Interface](#accessing-the-web-interface)
|
|
||||||
- [Connecting to the Service via Python Client](#connecting-to-the-service-via-python-client)
|
|
||||||
- [Tab Completion Support](#tab-completion-support)
|
|
||||||
- [Integration within Another Service](#integration-within-another-service)
|
|
||||||
- [RESTful API](#restful-api)
|
|
||||||
- [Understanding the Component System](#understanding-the-component-system)
|
|
||||||
- [Built-in Type and Enum Components](#built-in-type-and-enum-components)
|
|
||||||
- [Method Components](#method-components)
|
|
||||||
- [DataService Instances (Nested Classes)](#dataservice-instances-nested-classes)
|
|
||||||
- [Custom Components (`pydase.components`)](#custom-components-pydasecomponents)
|
|
||||||
- [`DeviceConnection`](#deviceconnection)
|
|
||||||
- [Customizing Connection Logic](#customizing-connection-logic)
|
|
||||||
- [Reconnection Interval](#reconnection-interval)
|
|
||||||
- [`Image`](#image)
|
|
||||||
- [`NumberSlider`](#numberslider)
|
|
||||||
- [`ColouredEnum`](#colouredenum)
|
|
||||||
- [Extending with New Components](#extending-with-new-components)
|
|
||||||
- [Understanding Service Persistence](#understanding-service-persistence)
|
|
||||||
- [Controlling Property State Loading with `@load_state`](#controlling-property-state-loading-with-load_state)
|
|
||||||
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
|
|
||||||
- [Understanding Units in pydase](#understanding-units-in-pydase)
|
|
||||||
- [Using `validate_set` to Validate Property Setters](#using-validate_set-to-validate-property-setters)
|
|
||||||
- [Configuring pydase via Environment Variables](#configuring-pydase-via-environment-variables)
|
|
||||||
- [Customizing the Web Interface](#customizing-the-web-interface)
|
|
||||||
- [Logging in pydase](#logging-in-pydase)
|
|
||||||
- [Changing the Log Level](#changing-the-log-level)
|
|
||||||
- [Documentation](#documentation)
|
|
||||||
- [Contributing](#contributing)
|
|
||||||
- [License](#license)
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<!-- no toc -->
|
<!-- no toc -->
|
||||||
- [Simple service definition through class-based interface](#defining-a-dataService)
|
- [Simple service definition through class-based interface](#defining-a-dataService)
|
||||||
- [Integrated web interface for interactive access and control of your service](#accessing-the-web-interface)
|
- [Auto-generated web interface for interactive access and control of your service](#accessing-the-web-interface)
|
||||||
- [Support for programmatic control and interaction with your service](#connecting-to-the-service-via-python-client)
|
- [Python RPC client](#connecting-to-the-service-via-python-rpc-client)
|
||||||
- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
|
- [Customizable web interface](#customizing-the-web-interface)
|
||||||
- [Customizable styling for the web interface](#customizing-web-interface-style)
|
- [Saving and restoring the service state](#understanding-service-persistence)
|
||||||
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
|
||||||
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
||||||
- [Support for units](#understanding-units-in-pydase)
|
- [Support for units](#understanding-units-in-pydase)
|
||||||
- [Validating Property Setters](#using-validate_set-to-validate-property-setters)
|
- [Validating Property Setters](#using-validate_set-to-validate-property-setters)
|
||||||
@ -84,16 +49,19 @@ Using `pydase` involves three main steps: defining a `DataService` subclass, run
|
|||||||
|
|
||||||
### Defining a DataService
|
### Defining a DataService
|
||||||
|
|
||||||
To use pydase, you'll first need to create a class that inherits from `DataService`. This class represents your custom data service, which will be exposed via a web server. Your class can implement class / instance attributes and synchronous and asynchronous tasks.
|
To use `pydase`, you'll first need to create a class that inherits from `pydase.DataService`.
|
||||||
|
This class represents your custom service, which will be exposed via a web server.<br>
|
||||||
|
Your class can implement synchronous and asynchronous methods, some [built-in types](https://docs.python.org/3/library/stdtypes.html) (like `int`, `float`, `str`, `bool`, `list` or `dict`) and [other components](https://pydase.readthedocs.io/en/latest/user-guide/Components/#custom-components-pydasecomponents) as attributes.
|
||||||
|
For more information, please refer to the [documentation](https://pydase.readthedocs.io/en/latest/user-guide/Components/).
|
||||||
|
|
||||||
Here's an example:
|
Here's an example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pydase import DataService, Server
|
import pydase
|
||||||
from pydase.utils.decorators import frontend
|
from pydase.utils.decorators import frontend
|
||||||
|
|
||||||
|
|
||||||
class Device(DataService):
|
class Device(pydase.DataService):
|
||||||
_current = 0.0
|
_current = 0.0
|
||||||
_voltage = 0.0
|
_voltage = 0.0
|
||||||
_power = False
|
_power = False
|
||||||
@ -136,26 +104,27 @@ class Device(DataService):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service = Device()
|
service = Device()
|
||||||
Server(service).run()
|
pydase.Server(service=service).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
In the above example, we define a Device class that extends DataService. We define a few properties (current, voltage, power) and their getter and setter methods.
|
In the above example, we define a `Device` class that inherits from `pydase.DataService`.
|
||||||
|
We define a few properties (current, voltage, power) and their getter and setter methods.
|
||||||
|
|
||||||
### Running the Server
|
### Running the Server
|
||||||
|
|
||||||
Once your DataService is defined, you can create an instance of it and run the server:
|
Once your service class is defined, you can create an instance of it and run the server:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from pydase import Server
|
import pydase
|
||||||
|
|
||||||
# ... defining the Device class ...
|
# ... defining the Device class ...
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service = Device()
|
service = Device()
|
||||||
Server(service).run()
|
pydase.Server(service=service).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the server, making your Device service accessible on [http://localhost:8001](http://localhost:8001).
|
This will start the server, making your `Device` service accessible on [http://localhost:8001](http://localhost:8001).
|
||||||
|
|
||||||
### Accessing the Web Interface
|
### Accessing the Web Interface
|
||||||
|
|
||||||
@ -209,445 +178,6 @@ For more information, see [here](https://pydase.readthedocs.io/en/stable/user-gu
|
|||||||
|
|
||||||
<!--usage-end-->
|
<!--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`.
|
|
||||||
- `list`: Each item displayed individually, named after the list attribute and its index.
|
|
||||||
- `dict`: Each key-value pair displayed individually, named after the dictionary attribute and its key. **Note** that the dictionary keys must be strings.
|
|
||||||
- `enum.Enum`: Presented as an `EnumComponent`, facilitating dropdown selection.
|
|
||||||
|
|
||||||
### Method Components
|
|
||||||
Within the `DataService` class of `pydase`, only methods devoid of arguments can be represented in the frontend, classified into two distinct categories
|
|
||||||
|
|
||||||
1. [**Tasks**](#understanding-tasks-in-pydase): Argument-free asynchronous functions, identified within `pydase` as tasks, are inherently designed for frontend interaction. These tasks are automatically rendered with a start/stop button, allowing users to initiate or halt the task execution directly from the web interface.
|
|
||||||
2. **Synchronous Methods with `@frontend` Decorator**: Synchronous methods without arguments can also be presented in the frontend. For this, they have to be decorated with the `@frontend` decorator.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pydase
|
|
||||||
import pydase.components
|
|
||||||
import pydase.units as u
|
|
||||||
from pydase.utils.decorators import frontend
|
|
||||||
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
|
||||||
@frontend
|
|
||||||
def exposed_method(self) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
async def my_task(self) -> None:
|
|
||||||
while True:
|
|
||||||
# ...
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
You can still define synchronous tasks with arguments and call them using a python client. However, decorating them with the `@frontend` decorator will raise a `FunctionDefinitionError`. Defining a task with arguments will raise a `TaskDefinitionError`.
|
|
||||||
I decided against supporting function arguments for functions rendered in the frontend due to the following reasons:
|
|
||||||
|
|
||||||
1. Feature Request Pitfall: supporting function arguments create a bottomless pit of feature requests. As users encounter the limitations of supported types, demands for extending support to more complex types would grow.
|
|
||||||
2. Complexity in Supported Argument Types: while simple types like `int`, `float`, `bool` and `str` could be easily supported, more complicated types are not (representation, (de-)serialization).
|
|
||||||
|
|
||||||
### DataService Instances (Nested Classes)
|
|
||||||
|
|
||||||
Nested `DataService` instances offer an organized hierarchy for components, enabling richer applications. Each nested class might have its own attributes and methods, each mapped to a frontend component.
|
|
||||||
|
|
||||||
Here is an example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from pydase import DataService, Server
|
|
||||||
|
|
||||||
|
|
||||||
class Channel(DataService):
|
|
||||||
def __init__(self, channel_id: int) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self._channel_id = channel_id
|
|
||||||
self._current = 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def current(self) -> float:
|
|
||||||
# run code to get current
|
|
||||||
result = self._current
|
|
||||||
return result
|
|
||||||
|
|
||||||
@current.setter
|
|
||||||
def current(self, value: float) -> None:
|
|
||||||
# run code to set current
|
|
||||||
self._current = value
|
|
||||||
|
|
||||||
|
|
||||||
class Device(DataService):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.channels = [Channel(i) for i in range(2)]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
service = Device()
|
|
||||||
Server(service).run()
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
|
|
||||||
|
|
||||||
### Custom Components (`pydase.components`)
|
|
||||||
|
|
||||||
The custom components in `pydase` have two main parts:
|
|
||||||
|
|
||||||
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
|
|
||||||
- A **Frontend React Component** that renders and manages user interaction in the browser.
|
|
||||||
|
|
||||||
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
|
|
||||||
|
|
||||||
#### `DeviceConnection`
|
|
||||||
|
|
||||||
The `DeviceConnection` component acts as a base class within the `pydase` framework for managing device connections. It provides a structured approach to handle connections by offering a customizable `connect` method and a `connected` property. This setup facilitates the implementation of automatic reconnection logic, which periodically attempts reconnection whenever the connection is lost.
|
|
||||||
|
|
||||||
In the frontend, this class abstracts away the direct interaction with the `connect` method and the `connected` property. Instead, it showcases user-defined attributes, methods, and properties. When the `connected` status is `False`, the frontend displays an overlay that prompts manual reconnection through the `connect()` method. Successful reconnection removes the overlay.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pydase.components
|
|
||||||
import pydase.units as u
|
|
||||||
|
|
||||||
|
|
||||||
class Device(pydase.components.DeviceConnection):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self._voltage = 10 * u.units.V
|
|
||||||
|
|
||||||
def connect(self) -> None:
|
|
||||||
if not self._connected:
|
|
||||||
self._connected = True
|
|
||||||
|
|
||||||
@property
|
|
||||||
def voltage(self) -> float:
|
|
||||||
return self._voltage
|
|
||||||
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.device = Device()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
service_instance = MyService()
|
|
||||||
pydase.Server(service_instance).run()
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
##### Customizing Connection Logic
|
|
||||||
|
|
||||||
Users are encouraged to primarily override the `connect` method to tailor the connection process to their specific device. This method should adjust the `self._connected` attribute based on the outcome of the connection attempt:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pydase.components
|
|
||||||
|
|
||||||
|
|
||||||
class MyDeviceConnection(pydase.components.DeviceConnection):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
# Add any necessary initialization code here
|
|
||||||
|
|
||||||
def connect(self) -> None:
|
|
||||||
# Implement device-specific connection logic here
|
|
||||||
# Update self._connected to `True` if the connection is successful,
|
|
||||||
# or `False` if unsuccessful
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
Moreover, if the connection status requires additional logic, users can override the `connected` property:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pydase.components
|
|
||||||
|
|
||||||
class MyDeviceConnection(pydase.components.DeviceConnection):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
# Add any necessary initialization code here
|
|
||||||
|
|
||||||
def connect(self) -> None:
|
|
||||||
# Implement device-specific connection logic here
|
|
||||||
# Ensure self._connected reflects the connection status accurately
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def connected(self) -> bool:
|
|
||||||
# Implement custom logic to accurately report connection status
|
|
||||||
return self._connected
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Reconnection Interval
|
|
||||||
|
|
||||||
The `DeviceConnection` component automatically executes a task that checks for device availability at a default interval of 10 seconds. This interval is adjustable by modifying the `_reconnection_wait_time` attribute on the class instance.
|
|
||||||
|
|
||||||
#### `Image`
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
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 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()
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
#### `NumberSlider`
|
|
||||||
|
|
||||||
The `NumberSlider` component in the `pydase` package provides an interactive slider interface for adjusting numerical values on the frontend. It is designed to support both numbers and quantities and ensures that values adjusted on the frontend are synchronized with the backend.
|
|
||||||
|
|
||||||
To utilize the `NumberSlider`, users should implement a class that derives from `NumberSlider`. This class can then define the initial values, minimum and maximum limits, step sizes, and additional logic as needed.
|
|
||||||
|
|
||||||
Here's an example of how to implement and use a custom slider:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pydase
|
|
||||||
import pydase.components
|
|
||||||
|
|
||||||
|
|
||||||
class MySlider(pydase.components.NumberSlider):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
value: float = 0.0,
|
|
||||||
min_: float = 0.0,
|
|
||||||
max_: float = 100.0,
|
|
||||||
step_size: float = 1.0,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(value, min_, max_, step_size)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def min(self) -> float:
|
|
||||||
return self._min
|
|
||||||
|
|
||||||
@min.setter
|
|
||||||
def min(self, value: float) -> None:
|
|
||||||
self._min = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def max(self) -> float:
|
|
||||||
return self._max
|
|
||||||
|
|
||||||
@max.setter
|
|
||||||
def max(self, value: float) -> None:
|
|
||||||
self._max = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def step_size(self) -> float:
|
|
||||||
return self._step_size
|
|
||||||
|
|
||||||
@step_size.setter
|
|
||||||
def step_size(self, value: float) -> None:
|
|
||||||
self._step_size = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self) -> float:
|
|
||||||
"""Slider value."""
|
|
||||||
return self._value
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value: float) -> None:
|
|
||||||
if value < self._min or value > self._max:
|
|
||||||
raise ValueError("Value is either below allowed min or above max value.")
|
|
||||||
|
|
||||||
self._value = value
|
|
||||||
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.voltage = MySlider()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
service_instance = MyService()
|
|
||||||
service_instance.voltage.value = 5
|
|
||||||
print(service_instance.voltage.value) # Output: 5
|
|
||||||
pydase.Server(service_instance).run()
|
|
||||||
```
|
|
||||||
|
|
||||||
In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` properties. Users can make any of these properties read-only by omitting the corresponding setter method.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
- Accessing parent class resources in `NumberSlider`
|
|
||||||
|
|
||||||
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
|
|
||||||
|
|
||||||
Here's an illustrative example:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
import pydase
|
|
||||||
import pydase.components
|
|
||||||
|
|
||||||
|
|
||||||
class MySlider(pydase.components.NumberSlider):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
value: float,
|
|
||||||
on_change: Callable[[float], None],
|
|
||||||
) -> None:
|
|
||||||
super().__init__(value=value)
|
|
||||||
self._on_change = on_change
|
|
||||||
|
|
||||||
# ... other properties ...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self) -> float:
|
|
||||||
return self._value
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, new_value: float) -> None:
|
|
||||||
if new_value < self._min or new_value > self._max:
|
|
||||||
raise ValueError("Value is either below allowed min or above max value.")
|
|
||||||
self._value = new_value
|
|
||||||
self._on_change(new_value)
|
|
||||||
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
self.voltage = MySlider(
|
|
||||||
5,
|
|
||||||
on_change=self.handle_voltage_change,
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle_voltage_change(self, new_voltage: float) -> None:
|
|
||||||
print(f"Voltage changed to: {new_voltage}")
|
|
||||||
# Additional logic here
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
service_instance = MyService()
|
|
||||||
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
|
|
||||||
pydase.Server(service_instance).run()
|
|
||||||
```
|
|
||||||
|
|
||||||
- Incorporating units in `NumberSlider`
|
|
||||||
|
|
||||||
The `NumberSlider` is capable of [displaying units](#understanding-units-in-pydase) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend.
|
|
||||||
|
|
||||||
Here's how to implement a `NumberSlider` with unit display:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import pydase
|
|
||||||
import pydase.components
|
|
||||||
import pydase.units as u
|
|
||||||
|
|
||||||
class MySlider(pydase.components.NumberSlider):
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
value: u.Quantity = 0.0 * u.units.V,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(value)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self) -> u.Quantity:
|
|
||||||
return self._value
|
|
||||||
|
|
||||||
@value.setter
|
|
||||||
def value(self, value: u.Quantity) -> None:
|
|
||||||
if value.m < self._min or value.m > self._max:
|
|
||||||
raise ValueError("Value is either below allowed min or above max value.")
|
|
||||||
self._value = value
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__()
|
|
||||||
self.voltage = MySlider()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
service_instance = MyService()
|
|
||||||
service_instance.voltage.value = 5 * u.units.V
|
|
||||||
print(service_instance.voltage.value) # Output: 5 V
|
|
||||||
pydase.Server(service_instance).run()
|
|
||||||
```
|
|
||||||
|
|
||||||
#### `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
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
**Note** that each enumeration name and value must be unique.
|
|
||||||
This means that you should use different colour formats when you want to use a colour multiple times.
|
|
||||||
|
|
||||||
#### 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
|
## 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.
|
`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.
|
||||||
|
@ -1,6 +1,435 @@
|
|||||||
# Components Guide
|
# Components Guide
|
||||||
{%
|
|
||||||
include-markdown "../../README.md"
|
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:
|
||||||
start="<!-- Component User Guide Start -->"
|
|
||||||
end="<!-- Component User Guide End -->"
|
## 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`.
|
||||||
|
- `list`: Each item displayed individually, named after the list attribute and its index.
|
||||||
|
- `dict`: Each key-value pair displayed individually, named after the dictionary attribute and its key. **Note** that the dictionary keys must be strings.
|
||||||
|
- `enum.Enum`: Presented as an `EnumComponent`, facilitating dropdown selection.
|
||||||
|
|
||||||
|
## Method Components
|
||||||
|
Within the `DataService` class of `pydase`, only methods devoid of arguments can be represented in the frontend, classified into two distinct categories
|
||||||
|
|
||||||
|
1. [**Tasks**](#understanding-tasks-in-pydase): Argument-free asynchronous functions, identified within `pydase` as tasks, are inherently designed for frontend interaction. These tasks are automatically rendered with a start/stop button, allowing users to initiate or halt the task execution directly from the web interface.
|
||||||
|
2. **Synchronous Methods with `@frontend` Decorator**: Synchronous methods without arguments can also be presented in the frontend. For this, they have to be decorated with the `@frontend` decorator.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase
|
||||||
|
import pydase.components
|
||||||
|
import pydase.units as u
|
||||||
|
from pydase.utils.decorators import frontend
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
@frontend
|
||||||
|
def exposed_method(self) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
async def my_task(self) -> None:
|
||||||
|
while True:
|
||||||
|
# ...
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
You can still define synchronous tasks with arguments and call them using a python client. However, decorating them with the `@frontend` decorator will raise a `FunctionDefinitionError`. Defining a task with arguments will raise a `TaskDefinitionError`.
|
||||||
|
I decided against supporting function arguments for functions rendered in the frontend due to the following reasons:
|
||||||
|
|
||||||
|
1. Feature Request Pitfall: supporting function arguments create a bottomless pit of feature requests. As users encounter the limitations of supported types, demands for extending support to more complex types would grow.
|
||||||
|
2. Complexity in Supported Argument Types: while simple types like `int`, `float`, `bool` and `str` could be easily supported, more complicated types are not (representation, (de-)serialization).
|
||||||
|
|
||||||
|
## DataService Instances (Nested Classes)
|
||||||
|
|
||||||
|
Nested `DataService` instances offer an organized hierarchy for components, enabling richer applications. Each nested class might have its own attributes and methods, each mapped to a frontend component.
|
||||||
|
|
||||||
|
Here is an example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pydase import DataService, Server
|
||||||
|
|
||||||
|
|
||||||
|
class Channel(DataService):
|
||||||
|
def __init__(self, channel_id: int) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._channel_id = channel_id
|
||||||
|
self._current = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current(self) -> float:
|
||||||
|
# run code to get current
|
||||||
|
result = self._current
|
||||||
|
return result
|
||||||
|
|
||||||
|
@current.setter
|
||||||
|
def current(self, value: float) -> None:
|
||||||
|
# run code to set current
|
||||||
|
self._current = value
|
||||||
|
|
||||||
|
|
||||||
|
class Device(DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.channels = [Channel(i) for i in range(2)]
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service = Device()
|
||||||
|
Server(service).run()
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
|
||||||
|
|
||||||
|
## Custom Components (`pydase.components`)
|
||||||
|
|
||||||
|
The custom components in `pydase` have two main parts:
|
||||||
|
|
||||||
|
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
|
||||||
|
- A **Frontend React Component** that renders and manages user interaction in the browser.
|
||||||
|
|
||||||
|
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
|
||||||
|
|
||||||
|
### `DeviceConnection`
|
||||||
|
|
||||||
|
The `DeviceConnection` component acts as a base class within the `pydase` framework for managing device connections. It provides a structured approach to handle connections by offering a customizable `connect` method and a `connected` property. This setup facilitates the implementation of automatic reconnection logic, which periodically attempts reconnection whenever the connection is lost.
|
||||||
|
|
||||||
|
In the frontend, this class abstracts away the direct interaction with the `connect` method and the `connected` property. Instead, it showcases user-defined attributes, methods, and properties. When the `connected` status is `False`, the frontend displays an overlay that prompts manual reconnection through the `connect()` method. Successful reconnection removes the overlay.
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase.components
|
||||||
|
import pydase.units as u
|
||||||
|
|
||||||
|
|
||||||
|
class Device(pydase.components.DeviceConnection):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._voltage = 10 * u.units.V
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
if not self._connected:
|
||||||
|
self._connected = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def voltage(self) -> float:
|
||||||
|
return self._voltage
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.device = Device()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service_instance = MyService()
|
||||||
|
pydase.Server(service_instance).run()
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
#### Customizing Connection Logic
|
||||||
|
|
||||||
|
Users are encouraged to primarily override the `connect` method to tailor the connection process to their specific device. This method should adjust the `self._connected` attribute based on the outcome of the connection attempt:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase.components
|
||||||
|
|
||||||
|
|
||||||
|
class MyDeviceConnection(pydase.components.DeviceConnection):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
# Add any necessary initialization code here
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
# Implement device-specific connection logic here
|
||||||
|
# Update self._connected to `True` if the connection is successful,
|
||||||
|
# or `False` if unsuccessful
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Moreover, if the connection status requires additional logic, users can override the `connected` property:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase.components
|
||||||
|
|
||||||
|
class MyDeviceConnection(pydase.components.DeviceConnection):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
# Add any necessary initialization code here
|
||||||
|
|
||||||
|
def connect(self) -> None:
|
||||||
|
# Implement device-specific connection logic here
|
||||||
|
# Ensure self._connected reflects the connection status accurately
|
||||||
|
...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def connected(self) -> bool:
|
||||||
|
# Implement custom logic to accurately report connection status
|
||||||
|
return self._connected
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Reconnection Interval
|
||||||
|
|
||||||
|
The `DeviceConnection` component automatically executes a task that checks for device availability at a default interval of 10 seconds. This interval is adjustable by modifying the `_reconnection_wait_time` attribute on the class instance.
|
||||||
|
|
||||||
|
### `Image`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
|
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 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()
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### `NumberSlider`
|
||||||
|
|
||||||
|
The `NumberSlider` component in the `pydase` package provides an interactive slider interface for adjusting numerical values on the frontend. It is designed to support both numbers and quantities and ensures that values adjusted on the frontend are synchronized with the backend.
|
||||||
|
|
||||||
|
To utilize the `NumberSlider`, users should implement a class that derives from `NumberSlider`. This class can then define the initial values, minimum and maximum limits, step sizes, and additional logic as needed.
|
||||||
|
|
||||||
|
Here's an example of how to implement and use a custom slider:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase
|
||||||
|
import pydase.components
|
||||||
|
|
||||||
|
|
||||||
|
class MySlider(pydase.components.NumberSlider):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: float = 0.0,
|
||||||
|
min_: float = 0.0,
|
||||||
|
max_: float = 100.0,
|
||||||
|
step_size: float = 1.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value, min_, max_, step_size)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def min(self) -> float:
|
||||||
|
return self._min
|
||||||
|
|
||||||
|
@min.setter
|
||||||
|
def min(self, value: float) -> None:
|
||||||
|
self._min = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max(self) -> float:
|
||||||
|
return self._max
|
||||||
|
|
||||||
|
@max.setter
|
||||||
|
def max(self, value: float) -> None:
|
||||||
|
self._max = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def step_size(self) -> float:
|
||||||
|
return self._step_size
|
||||||
|
|
||||||
|
@step_size.setter
|
||||||
|
def step_size(self, value: float) -> None:
|
||||||
|
self._step_size = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
"""Slider value."""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: float) -> None:
|
||||||
|
if value < self._min or value > self._max:
|
||||||
|
raise ValueError("Value is either below allowed min or above max value.")
|
||||||
|
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.voltage = MySlider()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service_instance = MyService()
|
||||||
|
service_instance.voltage.value = 5
|
||||||
|
print(service_instance.voltage.value) # Output: 5
|
||||||
|
pydase.Server(service_instance).run()
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` properties. Users can make any of these properties read-only by omitting the corresponding setter method.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Accessing parent class resources in `NumberSlider`
|
||||||
|
|
||||||
|
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
|
||||||
|
|
||||||
|
Here's an illustrative example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import pydase
|
||||||
|
import pydase.components
|
||||||
|
|
||||||
|
|
||||||
|
class MySlider(pydase.components.NumberSlider):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: float,
|
||||||
|
on_change: Callable[[float], None],
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value=value)
|
||||||
|
self._on_change = on_change
|
||||||
|
|
||||||
|
# ... other properties ...
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> float:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, new_value: float) -> None:
|
||||||
|
if new_value < self._min or new_value > self._max:
|
||||||
|
raise ValueError("Value is either below allowed min or above max value.")
|
||||||
|
self._value = new_value
|
||||||
|
self._on_change(new_value)
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.voltage = MySlider(
|
||||||
|
5,
|
||||||
|
on_change=self.handle_voltage_change,
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle_voltage_change(self, new_voltage: float) -> None:
|
||||||
|
print(f"Voltage changed to: {new_voltage}")
|
||||||
|
# Additional logic here
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service_instance = MyService()
|
||||||
|
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
|
||||||
|
pydase.Server(service_instance).run()
|
||||||
|
```
|
||||||
|
|
||||||
|
- Incorporating units in `NumberSlider`
|
||||||
|
|
||||||
|
The `NumberSlider` is capable of [displaying units](#understanding-units-in-pydase) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend.
|
||||||
|
|
||||||
|
Here's how to implement a `NumberSlider` with unit display:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase
|
||||||
|
import pydase.components
|
||||||
|
import pydase.units as u
|
||||||
|
|
||||||
|
class MySlider(pydase.components.NumberSlider):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
value: u.Quantity = 0.0 * u.units.V,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self) -> u.Quantity:
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@value.setter
|
||||||
|
def value(self, value: u.Quantity) -> None:
|
||||||
|
if value.m < self._min or value.m > self._max:
|
||||||
|
raise ValueError("Value is either below allowed min or above max value.")
|
||||||
|
self._value = value
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.voltage = MySlider()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service_instance = MyService()
|
||||||
|
service_instance.voltage.value = 5 * u.units.V
|
||||||
|
print(service_instance.voltage.value) # Output: 5 V
|
||||||
|
pydase.Server(service_instance).run()
|
||||||
|
```
|
||||||
|
|
||||||
|
### `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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Note** that each enumeration name and value must be unique.
|
||||||
|
This means that you should use different colour formats when you want to use a colour multiple times.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user