From 7904d0d7d97d6db27a903b1b35fdc138f82cbeb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 08:50:46 +0200 Subject: [PATCH 01/11] updates Readme introduction --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 81e138b..c94a785 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # pydase -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=latest) +[![Version](https://img.shields.io/pypi/v/pydase?style=flat)](https://pypi.org/project/pydase/) +[![Python Versions](https://img.shields.io/pypi/pyversions/pydase)](https://pypi.org/project/pydase/) +[![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=stable) +[![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)](https://github.com/tiqi-group/pydase/blob/main/LICENSE) -`pydase` is a Python library designed to streamline the creation of services that interface with devices and data. It offers a unified API, simplifying the process of data querying and device interaction. Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates rapid service development and deployment. +`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. + +Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates service development and deployment. - [Features](#features) - [Installation](#installation) From ca19fcc63fd8f0a93ee1539ad96e500edd336bff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 14:11:26 +0200 Subject: [PATCH 02/11] updates Readme (moving components guide to docs, removing TOC, updated features list,...) --- README.md | 508 ++-------------------------------- docs/user-guide/Components.md | 439 ++++++++++++++++++++++++++++- 2 files changed, 453 insertions(+), 494 deletions(-) diff --git a/README.md b/README.md index c94a785..cc9d93f 100644 --- a/README.md +++ b/README.md @@ -5,54 +5,19 @@ [![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=stable) [![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)](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. -- [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 - [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) -- [Support for programmatic control and interaction with your service](#connecting-to-the-service-via-python-client) -- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system) -- [Customizable styling for the web interface](#customizing-web-interface-style) -- [Saving and restoring the service state for service persistence](#understanding-service-persistence) +- [Auto-generated web interface for interactive access and control of your service](#accessing-the-web-interface) +- [Python RPC client](#connecting-to-the-service-via-python-rpc-client) +- [Customizable web interface](#customizing-the-web-interface) +- [Saving and restoring the service state](#understanding-service-persistence) - [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase) - [Support for units](#understanding-units-in-pydase) - [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 -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.
+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: ```python -from pydase import DataService, Server +import pydase from pydase.utils.decorators import frontend -class Device(DataService): +class Device(pydase.DataService): _current = 0.0 _voltage = 0.0 _power = False @@ -136,26 +104,27 @@ class Device(DataService): if __name__ == "__main__": 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 -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 -from pydase import Server +import pydase # ... defining the Device class ... if __name__ == "__main__": 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 @@ -209,445 +178,6 @@ For more information, see [here](https://pydase.readthedocs.io/en/stable/user-gu -## Understanding the Component System - - - -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: - # ... -``` - -![Method Components](docs/images/method_components.png) - -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() -``` - -![Nested Classes App](docs/images/Nested_Class_App.png) - -**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() -``` - -![DeviceConnection Component](docs/images/DeviceConnection_component.png) - -##### 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() -``` - -![Image Component](docs/images/Image_component.png) - -#### `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. - -![Slider Component](docs/images/Slider_component.png) - -- 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 -``` - -![ColouredEnum Component](docs/images/ColouredEnum_component.png) - -**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. - - - ## 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. diff --git a/docs/user-guide/Components.md b/docs/user-guide/Components.md index 4bfa4e0..2cb3714 100644 --- a/docs/user-guide/Components.md +++ b/docs/user-guide/Components.md @@ -1,6 +1,435 @@ # Components Guide -{% - include-markdown "../../README.md" - start="" - end="" -%} + +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: + # ... +``` + +![Method Components](docs/images/method_components.png) + +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() +``` + +![Nested Classes App](docs/images/Nested_Class_App.png) + +**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() +``` + +![DeviceConnection Component](docs/images/DeviceConnection_component.png) + +#### 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() +``` + +![Image Component](docs/images/Image_component.png) + +### `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. + +![Slider Component](docs/images/Slider_component.png) + +- 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 +``` + +![ColouredEnum Component](docs/images/ColouredEnum_component.png) + +**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. + From c7ec929d054a3e8c2c667982016688ffe01ebfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 14:45:56 +0200 Subject: [PATCH 03/11] moves state persistence section into docs, restructuring docs --- README.md | 63 ++++---------------------- docs/getting-started.md | 12 +---- docs/index.md | 6 ++- docs/user-guide/Components.md | 12 ++--- docs/user-guide/Service_Persistence.md | 49 ++++++++++++++++++++ 5 files changed, 70 insertions(+), 72 deletions(-) create mode 100644 docs/user-guide/Service_Persistence.md diff --git a/README.md b/README.md index cc9d93f..814a0bb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # pydase [![Version](https://img.shields.io/pypi/v/pydase?style=flat)](https://pypi.org/project/pydase/) @@ -17,15 +18,18 @@ Whether you're managing lab sensors, network devices, or any abstract data entit - [Auto-generated web interface for interactive access and control of your service](#accessing-the-web-interface) - [Python RPC client](#connecting-to-the-service-via-python-rpc-client) - [Customizable web interface](#customizing-the-web-interface) -- [Saving and restoring the service state](#understanding-service-persistence) +- [Saving and restoring the service state](https://pydase.readthedocs.io/en/latest/user-guide/Service_Persistence) - [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase) - [Support for units](#understanding-units-in-pydase) - [Validating Property Setters](#using-validate_set-to-validate-property-setters) + + + + ## Installation - Install `pydase` using [`poetry`](https://python-poetry.org/): @@ -39,13 +43,11 @@ or `pip`: pip install pydase ``` - ## Usage - -Using `pydase` involves three main steps: defining a `DataService` subclass, running the server, and then connecting to the service either programmatically using `pydase.Client` or through the web interface. +Using `pydase` involves three main steps: defining a `pydase.DataService` subclass, running the server, and then connecting to the service either programmatically using `pydase.Client` or through the web interface. ### Defining a DataService @@ -176,56 +178,7 @@ serialized_value = json.loads(response.text) For more information, see [here](https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api). - - -## 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. - -To save the state of your service, pass a `filename` keyword argument to the constructor of the `pydase.Server` class. If the file specified by `filename` does not exist, the state manager will create this file and store its state in it when the service is shut down. If the file already exists, the state manager will load the state from this file, setting the values of its attributes to the values stored in the file. - -Here's an example: - -```python -from pydase import DataService, Server - -class Device(DataService): - # ... defining the Device class ... - - -if __name__ == "__main__": - service = Device() - Server(service, filename="device_state.json").run() -``` - -In this example, the state of the `Device` service will be saved to `device_state.json` when the service is shut down. If `device_state.json` exists when the server is started, the state manager will restore the state of the service from this file. - -### Controlling Property State Loading with `@load_state` - -By default, the state manager only restores values for public attributes of your service. If you have properties that you want to control the loading for, you can use the `@load_state` decorator on your property setters. This indicates to the state manager that the value of the property should be loaded from the state file. - -Here is how you can apply the `@load_state` decorator: - -```python -from pydase import DataService -from pydase.data_service.state_manager import load_state - -class Device(DataService): - _name = "Default Device Name" - - @property - def name(self) -> str: - return self._name - - @name.setter - @load_state - def name(self, value: str) -> None: - self._name = value -``` - -With the `@load_state` decorator applied to the `name` property setter, the state manager will load and apply the `name` property's value from the file storing the state upon server startup, assuming it exists. - -Note: If the service class structure has changed since the last time its state was saved, only the attributes and properties decorated with `@load_state` that have remained the same will be restored from the settings file. + ## Understanding Tasks in pydase diff --git a/docs/getting-started.md b/docs/getting-started.md index 177a47e..fe67a7b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,14 +1,6 @@ # Getting Started -## Installation {% include-markdown "../README.md" - start="" - end="" + start="" + end="" %} - -## Usage -{% - include-markdown "../README.md" - start="" - end="" -%} \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 96d83c6..afbc42a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1 +1,5 @@ -{% include-markdown "../README.md" %} \ No newline at end of file +{% + include-markdown "../README.md" + start="" + end="" +%} diff --git a/docs/user-guide/Components.md b/docs/user-guide/Components.md index 2cb3714..b1c74ec 100644 --- a/docs/user-guide/Components.md +++ b/docs/user-guide/Components.md @@ -36,7 +36,7 @@ class MyService(pydase.DataService): # ... ``` -![Method Components](docs/images/method_components.png) +![Method Components](../images/method_components.png) 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: @@ -83,7 +83,7 @@ if __name__ == "__main__": Server(service).run() ``` -![Nested Classes App](docs/images/Nested_Class_App.png) +![Nested Classes App](../images/Nested_Class_App.png) **Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)). @@ -132,7 +132,7 @@ if __name__ == "__main__": pydase.Server(service_instance).run() ``` -![DeviceConnection Component](docs/images/DeviceConnection_component.png) +![DeviceConnection Component](../images/DeviceConnection_component.png) #### Customizing Connection Logic @@ -214,7 +214,7 @@ if __name__ == "__main__": pydase.Server(service).run() ``` -![Image Component](docs/images/Image_component.png) +![Image Component](../images/Image_component.png) ### `NumberSlider` @@ -291,7 +291,7 @@ if __name__ == "__main__": 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. -![Slider Component](docs/images/Slider_component.png) +![Slider Component](../images/Slider_component.png) - Accessing parent class resources in `NumberSlider` @@ -424,7 +424,7 @@ my_service = StatusExample() my_service.status = MyStatus.FAILED ``` -![ColouredEnum Component](docs/images/ColouredEnum_component.png) +![ColouredEnum Component](../images/ColouredEnum_component.png) **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. diff --git a/docs/user-guide/Service_Persistence.md b/docs/user-guide/Service_Persistence.md new file mode 100644 index 0000000..af2236b --- /dev/null +++ b/docs/user-guide/Service_Persistence.md @@ -0,0 +1,49 @@ +# 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. + +To save the state of your service, pass a `filename` keyword argument to the constructor of the `pydase.Server` class. If the file specified by `filename` does not exist, the state manager will create this file and store its state in it when the service is shut down. If the file already exists, the state manager will load the state from this file, setting the values of its attributes to the values stored in the file. + +Here's an example: + +```python +import pydase + +class Device(pydase.DataService): + # ... defining the Device class ... + + +if __name__ == "__main__": + service = Device() + pydase.Server(service=service, filename="device_state.json").run() +``` + +In this example, the state of the `Device` service will be saved to `device_state.json` when the service is shut down. If `device_state.json` exists when the server is started, the state manager will restore the state of the service from this file. + +## Controlling Property State Loading with `@load_state` + +By default, the state manager only restores values for public attributes of your service. If you have properties that you want to control the loading for, you can use the `@load_state` decorator on your property setters. This indicates to the state manager that the value of the property should be loaded from the state file. + +Here is how you can apply the `@load_state` decorator: + +```python +import pydase +from pydase.data_service.state_manager import load_state + +class Device(pydase.DataService): + _name = "Default Device Name" + + @property + def name(self) -> str: + return self._name + + @name.setter + @load_state + def name(self, value: str) -> None: + self._name = value +``` + +With the `@load_state` decorator applied to the `name` property setter, the state manager will load and apply the `name` property's value from the file storing the state upon server startup, assuming it exists. + +Note: If the service class structure has changed since the last time its state was saved, only the attributes and properties decorated with `@load_state` that have remained the same will be restored from the settings file. + From 3eb9c6476b8869eb823e5845b315e2c556206721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:06:04 +0200 Subject: [PATCH 04/11] replaces inline links with reference links (can be overwritten in docs) --- README.md | 40 ++++++++++++++++++++++++++-------------- docs/getting-started.md | 5 +++++ docs/index.md | 9 +++++++++ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 814a0bb..b7579cd 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,14 @@ Whether you're managing lab sensors, network devices, or any abstract data entit ## Features -- [Simple service definition through class-based interface](#defining-a-dataService) -- [Auto-generated web interface for interactive access and control of your service](#accessing-the-web-interface) -- [Python RPC client](#connecting-to-the-service-via-python-rpc-client) -- [Customizable web interface](#customizing-the-web-interface) -- [Saving and restoring the service state](https://pydase.readthedocs.io/en/latest/user-guide/Service_Persistence) -- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase) -- [Support for units](#understanding-units-in-pydase) -- [Validating Property Setters](#using-validate_set-to-validate-property-setters) - +- [Simple service definition through class-based interface][Defining DataService] +- [Auto-generated web interface for interactive access and control of your service][Web Interface Access] +- [Python RPC client][Short RPC Client] +- [Customizable web interface][Customizing Web Interface] +- [Saving and restoring the service state][Service Persistence] +- [Automated task management with built-in start/stop controls and optional autostart][Task Management] +- [Support for units][Units] +- [Validating Property Setters][Property Validation] @@ -53,8 +52,8 @@ Using `pydase` involves three main steps: defining a `pydase.DataService` subcla 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.
-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/). +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][Custom Components] as attributes. +For more information, please refer to the [components guide][Components]. Here's an example: @@ -158,7 +157,7 @@ The proxy acts as a local representative of the remote service, enabling straigh The proxy class dynamically synchronizes with the server's exposed attributes. This synchronization allows the proxy to be automatically updated with any attributes or methods that the server exposes, essentially mirroring the server's API. This dynamic updating enables users to interact with the remote service as if they were working with a local object. -The RPC client also supports tab completion support in the interpreter, can be used as a context manager and integrates very well with other pydase services. For more information, please refer to the [documentation](https://pydase.readthedocs.io/en/latest/user-guide/interaction/main/#python-client). +The RPC client also supports tab completion support in the interpreter, can be used as a context manager and integrates very well with other pydase services. For more information, please refer to the [documentation][Python RPC Client]. ### RESTful API The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. @@ -176,7 +175,7 @@ response = requests.get( serialized_value = json.loads(response.text) ``` -For more information, see [here](https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api). +For more information, see [here][RESTful API]. @@ -410,7 +409,7 @@ You have two primary ways to adjust the log levels in `pydase`: ## Documentation -The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](https://pydase.readthedocs.io/en/latest/) for more information. +The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](https://pydase.readthedocs.io/en/stable/) for more information. ## Contributing @@ -419,3 +418,16 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc ## License `pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE). + +[Service Persistence]: https://pydase.readthedocs.io/en/stable/user-guide/Service_Persistence +[Defining DataService]: #defining-a-dataService +[Web Interface Access]: #accessing-the-web-interface +[Short RPC Client]: #connecting-to-the-service-via-python-rpc-client +[Customizing Web Interface]: #customizing-the-web-interface +[Task Management]: #understanding-tasks-in-pydase +[Units]: #understanding-units-in-pydase +[Property Validation]: #using-validate_set-to-validate-property-setters +[Custom Components]: https://pydase.readthedocs.io/en/latest/user-guide/Components/#custom-components-pydasecomponents +[Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/ +[RESTful API]: https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api +[Python RPC Client]: https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#python-rpc-client diff --git a/docs/getting-started.md b/docs/getting-started.md index fe67a7b..938d812 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -4,3 +4,8 @@ start="" end="" %} + +[RESTful API]: ./user-guide/interaction/main.md#restful-api +[Python RPC Client]: ./user-guide/interaction/main.md#python-rpc-client +[Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents +[Components]: ./user-guide/Components.md diff --git a/docs/index.md b/docs/index.md index afbc42a..8fffdb9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,3 +3,12 @@ start="" end="" %} + +[Service Persistence]: ./user-guide/Service_Persistence.md +[Defining DataService]: ./getting-started.md#defining-a-dataservice +[Web Interface Access]: ./getting-started.md#accessing-the-web-interface +[Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client +[Customizing Web Interface]: ./user-guide/interaction/main.md#customization-options +[Task Management]: ./getting-started.md#understanding-tasks-in-pydase +[Units]: ./getting-started.md#understanding-units-in-pydase +[Property Validation]: ./getting-started.md#using-validate_set-to-validate-property-setters From fb75de5b51738652fa7d52f7fcff8bfc31f53995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:19:46 +0200 Subject: [PATCH 05/11] adds service persistence page to mkdocs.yml --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 3b257d2..8abf81d 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,6 +7,7 @@ nav: - User Guide: - Components Guide: user-guide/Components.md - Interacting with pydase Services: user-guide/interaction/main.md + - Service Persistence: user-guide/Service_Persistence.md - Developer Guide: - Developer Guide: dev-guide/README.md - API Reference: dev-guide/api.md From 97e21b2ea8c473d8871eb9fc918d983d4302c45e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:34:09 +0200 Subject: [PATCH 06/11] docs: more reference links --- README.md | 5 +++-- docs/dev-guide/Observer_Pattern_Implementation.md | 2 +- docs/index.md | 1 + 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b7579cd..2789807 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ [![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=stable) [![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)](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, 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. +`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](https://python-socketio.readthedocs.io/en/stable/) 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][Observer Pattern] to provide the 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. @@ -419,6 +419,7 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc `pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE). +[Observer Pattern]: https://pydase.readthedocs.io/en/docs/dev-guide/Observer_Pattern_Implementation/ [Service Persistence]: https://pydase.readthedocs.io/en/stable/user-guide/Service_Persistence [Defining DataService]: #defining-a-dataService [Web Interface Access]: #accessing-the-web-interface diff --git a/docs/dev-guide/Observer_Pattern_Implementation.md b/docs/dev-guide/Observer_Pattern_Implementation.md index cb78a1e..8b9e072 100644 --- a/docs/dev-guide/Observer_Pattern_Implementation.md +++ b/docs/dev-guide/Observer_Pattern_Implementation.md @@ -2,7 +2,7 @@ ## Overview -The Observer Pattern is a fundamental design pattern in the `pydase` package, serving as the central communication mechanism for state updates to clients connected to a service. +The [Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern) is a fundamental design pattern in the `pydase` package, serving as the central communication mechanism for state updates to clients connected to a service. ## How it Works diff --git a/docs/index.md b/docs/index.md index 8fffdb9..8f6acf0 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,7 @@ end="" %} +[Observer Pattern]: ./dev-guide/Observer_Pattern_Implementation.md [Service Persistence]: ./user-guide/Service_Persistence.md [Defining DataService]: ./getting-started.md#defining-a-dataservice [Web Interface Access]: ./getting-started.md#accessing-the-web-interface From 9b8279da85cdf35f886b454d10ab3cca5f323d65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:41:19 +0200 Subject: [PATCH 07/11] moving "Understanding Tasks" into docs --- README.md | 40 +--------------------------------------- docs/index.md | 2 +- docs/user-guide/Tasks.md | 39 +++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 42 insertions(+), 40 deletions(-) create mode 100644 docs/user-guide/Tasks.md diff --git a/README.md b/README.md index 2789807..6dffb06 100644 --- a/README.md +++ b/README.md @@ -179,44 +179,6 @@ For more information, see [here][RESTful API]. -## Understanding Tasks in pydase - -In `pydase`, a task is defined as an asynchronous function without arguments contained in a class that inherits from `DataService`. These tasks usually contain a while loop and are designed to carry out periodic functions. - -For example, a task might be used to periodically read sensor data, update a database, or perform any other recurring job. One core feature of `pydase` is its ability to automatically generate start and stop functions for these tasks. This allows you to control task execution via both the frontend and python clients, giving you flexible and powerful control over your service's operation. - -Another powerful feature of `pydase` is its ability to automatically start tasks upon initialization of the service. By specifying the tasks and their arguments in the `_autostart_tasks` dictionary in your service class's `__init__` method, `pydase` will automatically start these tasks when the server is started. Here's an example: - -```python -from pydase import DataService, Server - -class SensorService(DataService): - def __init__(self): - super().__init__() - self.readout_frequency = 1.0 - self._autostart_tasks["read_sensor_data"] = () - - def _process_data(self, data: ...) -> None: - ... - - def _read_from_sensor(self) -> Any: - ... - - async def read_sensor_data(self): - while True: - data = self._read_from_sensor() - self._process_data(data) # Process the data as needed - await asyncio.sleep(self.readout_frequency) - - -if __name__ == "__main__": - service = SensorService() - Server(service).run() -``` - -In this example, `read_sensor_data` is a task that continuously reads data from a sensor. By adding it to the `_autostart_tasks` dictionary, it will automatically start running when `Server(service).run()` is executed. -As with all tasks, `pydase` will generate `start_read_sensor_data` and `stop_read_sensor_data` methods, which can be called to manually start and stop the data reading task. The readout frequency can be updated using the `readout_frequency` attribute. - ## Understanding Units in pydase `pydase` integrates with the [`pint`](https://pint.readthedocs.io/en/stable/) package to allow you to work with physical quantities within your service. This enables you to define attributes with units, making your service more expressive and ensuring consistency in the handling of physical quantities. @@ -425,7 +387,7 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc [Web Interface Access]: #accessing-the-web-interface [Short RPC Client]: #connecting-to-the-service-via-python-rpc-client [Customizing Web Interface]: #customizing-the-web-interface -[Task Management]: #understanding-tasks-in-pydase +[Task Management]: https://pydase.readthedocs.io/en/stable/user-guide/Tasks/ [Units]: #understanding-units-in-pydase [Property Validation]: #using-validate_set-to-validate-property-setters [Custom Components]: https://pydase.readthedocs.io/en/latest/user-guide/Components/#custom-components-pydasecomponents diff --git a/docs/index.md b/docs/index.md index 8f6acf0..0255a22 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,6 +10,6 @@ [Web Interface Access]: ./getting-started.md#accessing-the-web-interface [Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client [Customizing Web Interface]: ./user-guide/interaction/main.md#customization-options -[Task Management]: ./getting-started.md#understanding-tasks-in-pydase +[Task Management]: ./user-guide/Tasks.md [Units]: ./getting-started.md#understanding-units-in-pydase [Property Validation]: ./getting-started.md#using-validate_set-to-validate-property-setters diff --git a/docs/user-guide/Tasks.md b/docs/user-guide/Tasks.md new file mode 100644 index 0000000..3f62ed9 --- /dev/null +++ b/docs/user-guide/Tasks.md @@ -0,0 +1,39 @@ +# Understanding Tasks + +In `pydase`, a task is defined as an asynchronous function without arguments contained in a class that inherits from `pydase.DataService`. These tasks usually contain a while loop and are designed to carry out periodic functions. + +For example, a task might be used to periodically read sensor data, update a database, or perform any other recurring job. One core feature of `pydase` is its ability to automatically generate start and stop functions for these tasks. This allows you to control task execution via both the frontend and python clients, giving you flexible and powerful control over your service's operation. + +Another powerful feature of `pydase` is its ability to automatically start tasks upon initialization of the service. By specifying the tasks and their arguments in the `_autostart_tasks` dictionary in your service class's `__init__` method, `pydase` will automatically start these tasks when the server is started. Here's an example: + +```python +import pydase + + +class SensorService(pydase.DataService): + def __init__(self): + super().__init__() + self.readout_frequency = 1.0 + self._autostart_tasks["read_sensor_data"] = () + + def _process_data(self, data: ...) -> None: + ... + + def _read_from_sensor(self) -> Any: + ... + + async def read_sensor_data(self): + while True: + data = self._read_from_sensor() + self._process_data(data) # Process the data as needed + await asyncio.sleep(self.readout_frequency) + + +if __name__ == "__main__": + service = SensorService() + pydase.Server(service=service).run() +``` + +In this example, `read_sensor_data` is a task that continuously reads data from a sensor. By adding it to the `_autostart_tasks` dictionary, it will automatically start running when `pydase.Server(service).run()` is executed. +As with all tasks, `pydase` will generate `start_read_sensor_data` and `stop_read_sensor_data` methods, which can be called to manually start and stop the data reading task. The readout frequency can be updated using the `readout_frequency` attribute. + diff --git a/mkdocs.yml b/mkdocs.yml index 8abf81d..442edbf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -8,6 +8,7 @@ nav: - Components Guide: user-guide/Components.md - Interacting with pydase Services: user-guide/interaction/main.md - Service Persistence: user-guide/Service_Persistence.md + - Understanding Tasks: user-guide/Tasks.md - Developer Guide: - Developer Guide: dev-guide/README.md - API Reference: dev-guide/api.md From b0c3c4cad9dc2ca7e4c347352d0af82b99ad8386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:52:08 +0200 Subject: [PATCH 08/11] moves "Validating Property Setters" to docs --- README.md | 47 ++----------------- docs/index.md | 2 +- .../user-guide/Validating-Property-Setters.md | 38 +++++++++++++++ mkdocs.yml | 3 +- 4 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 docs/user-guide/Validating-Property-Setters.md diff --git a/README.md b/README.md index 6dffb06..e92125f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Version](https://img.shields.io/pypi/v/pydase?style=flat)](https://pypi.org/project/pydase/) [![Python Versions](https://img.shields.io/pypi/pyversions/pydase)](https://pypi.org/project/pydase/) -[![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=latest)](https://pydase.readthedocs.io/en/latest/?badge=stable) +[![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=stable)](https://pydase.readthedocs.io/en/stable/) [![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)](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](https://python-socketio.readthedocs.io/en/stable/) 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. @@ -244,45 +244,6 @@ if __name__ == "__main__": For more information about what you can do with the units, please consult the documentation of [`pint`](https://pint.readthedocs.io/en/stable/). -## Using `validate_set` to Validate Property Setters - -The `validate_set` decorator ensures that a property setter reads back the set value using the property getter and checks it against the desired value. -This decorator can be used to validate that a parameter has been correctly set on a device within a specified precision and timeout. - -The decorator takes two keyword arguments: `timeout` and `precision`. The `timeout` argument specifies the maximum time (in seconds) to wait for the value to be within the precision boundary. -If the value is not within the precision boundary after this time, an exception is raised. -The `precision` argument defines the acceptable deviation from the desired value. -If `precision` is `None`, the value must be exact. -For example, if `precision` is set to `1e-5`, the value read from the device must be within ±0.00001 of the desired value. - -Here’s how to use the `validate_set` decorator in a `DataService` class: - -```python -import pydase -from pydase.observer_pattern.observable.decorators import validate_set - - -class Service(pydase.DataService): - def __init__(self) -> None: - super().__init__() - self._device = RemoteDevice() # dummy class - - @property - def value(self) -> float: - # Implement how to get the value from the remote device... - return self._device.value - - @value.setter - @validate_set(timeout=1.0, precision=1e-5) - def value(self, value: float) -> None: - # Implement how to set the value on the remote device... - self._device.value = value - - -if __name__ == "__main__": - pydase.Server(Service()).run() -``` - ## Configuring pydase via Environment Variables Configuring `pydase` through environment variables enhances flexibility, security, and reusability. This approach allows for easy adaptation of services across different environments without code changes, promoting scalability and maintainability. With that, it simplifies deployment processes and facilitates centralized configuration management. Moreover, environment variables enable separation of configuration from code, aiding in secure and collaborative development. @@ -375,7 +336,7 @@ The full documentation provides more detailed information about `pydase`, includ ## Contributing -We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/latest/about/contributing/) for details on how to contribute. +We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/stable/about/contributing/) for details on how to contribute. ## License @@ -389,8 +350,8 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc [Customizing Web Interface]: #customizing-the-web-interface [Task Management]: https://pydase.readthedocs.io/en/stable/user-guide/Tasks/ [Units]: #understanding-units-in-pydase -[Property Validation]: #using-validate_set-to-validate-property-setters -[Custom Components]: https://pydase.readthedocs.io/en/latest/user-guide/Components/#custom-components-pydasecomponents +[Property Validation]: https://pydase.readthedocs.io/en/stable/user-guide/Validating-Property-Setters/ +[Custom Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/#custom-components-pydasecomponents [Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/ [RESTful API]: https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api [Python RPC Client]: https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#python-rpc-client diff --git a/docs/index.md b/docs/index.md index 0255a22..fd8f105 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,4 +12,4 @@ [Customizing Web Interface]: ./user-guide/interaction/main.md#customization-options [Task Management]: ./user-guide/Tasks.md [Units]: ./getting-started.md#understanding-units-in-pydase -[Property Validation]: ./getting-started.md#using-validate_set-to-validate-property-setters +[Property Validation]: ./user-guide/Validating-Property-Setters.md diff --git a/docs/user-guide/Validating-Property-Setters.md b/docs/user-guide/Validating-Property-Setters.md new file mode 100644 index 0000000..c114670 --- /dev/null +++ b/docs/user-guide/Validating-Property-Setters.md @@ -0,0 +1,38 @@ +# Using `validate_set` to Validate Property Setters + +The `validate_set` decorator ensures that a property setter reads back the set value using the property getter and checks it against the desired value. +This decorator can be used to validate that a parameter has been correctly set on a device within a specified precision and timeout. + +The decorator takes two keyword arguments: `timeout` and `precision`. The `timeout` argument specifies the maximum time (in seconds) to wait for the value to be within the precision boundary. +If the value is not within the precision boundary after this time, an exception is raised. +The `precision` argument defines the acceptable deviation from the desired value. +If `precision` is `None`, the value must be exact. +For example, if `precision` is set to `1e-5`, the value read from the device must be within ±0.00001 of the desired value. + +Here’s how to use the `validate_set` decorator in a `DataService` class: + +```python +import pydase +from pydase.observer_pattern.observable.decorators import validate_set + + +class Service(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self._device = RemoteDevice() # dummy class + + @property + def value(self) -> float: + # Implement how to get the value from the remote device... + return self._device.value + + @value.setter + @validate_set(timeout=1.0, precision=1e-5) + def value(self, value: float) -> None: + # Implement how to set the value on the remote device... + self._device.value = value + + +if __name__ == "__main__": + pydase.Server(service=Service()).run() +``` diff --git a/mkdocs.yml b/mkdocs.yml index 442edbf..736ea57 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,8 +7,9 @@ nav: - User Guide: - Components Guide: user-guide/Components.md - Interacting with pydase Services: user-guide/interaction/main.md - - Service Persistence: user-guide/Service_Persistence.md + - Achieving Service Persistence: user-guide/Service_Persistence.md - Understanding Tasks: user-guide/Tasks.md + - Validating Property Setters: user-guide/Validating-Property-Setters.md - Developer Guide: - Developer Guide: dev-guide/README.md - API Reference: dev-guide/api.md From 50f3686c12ff2e9822ad22396863e21d6924b55f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:56:57 +0200 Subject: [PATCH 09/11] moves "Understanding Units" to docs --- README.md | 67 +------------------------- docs/index.md | 2 +- docs/user-guide/Understanding-Units.md | 64 ++++++++++++++++++++++++ mkdocs.yml | 1 + 4 files changed, 67 insertions(+), 67 deletions(-) create mode 100644 docs/user-guide/Understanding-Units.md diff --git a/README.md b/README.md index e92125f..66f7a11 100644 --- a/README.md +++ b/README.md @@ -179,71 +179,6 @@ For more information, see [here][RESTful API]. -## Understanding Units in pydase - -`pydase` integrates with the [`pint`](https://pint.readthedocs.io/en/stable/) package to allow you to work with physical quantities within your service. This enables you to define attributes with units, making your service more expressive and ensuring consistency in the handling of physical quantities. - -You can define quantities in your `DataService` subclass using `pydase`'s `units` functionality. - -Here's an example: - -```python -from typing import Any - -import pydase.units as u -from pydase import DataService, Server - - -class ServiceClass(DataService): - voltage = 1.0 * u.units.V - _current: u.Quantity = 1.0 * u.units.mA - - @property - def current(self) -> u.Quantity: - return self._current - - @current.setter - def current(self, value: u.Quantity) -> None: - self._current = value - - -if __name__ == "__main__": - service = ServiceClass() - - service.voltage = 10.0 * u.units.V - service.current = 1.5 * u.units.mA - - Server(service).run() -``` - -In the frontend, quantities are rendered as floats, with the unit displayed as additional text. This allows you to maintain a clear and consistent representation of physical quantities across both the backend and frontend of your service. -![Web interface with rendered units](./docs/images/Units_App.png) - -Should you need to access the magnitude or the unit of a quantity, you can use the `.m` attribute or the `.u` attribute of the variable, respectively. For example, this could be necessary to set the periodicity of a task: - -```python -import asyncio -from pydase import DataService, Server -import pydase.units as u - - -class ServiceClass(DataService): - readout_wait_time = 1.0 * u.units.ms - - async def read_sensor_data(self): - while True: - print("Reading out sensor ...") - await asyncio.sleep(self.readout_wait_time.to("s").m) - - -if __name__ == "__main__": - service = ServiceClass() - - Server(service).run() -``` - -For more information about what you can do with the units, please consult the documentation of [`pint`](https://pint.readthedocs.io/en/stable/). - ## Configuring pydase via Environment Variables Configuring `pydase` through environment variables enhances flexibility, security, and reusability. This approach allows for easy adaptation of services across different environments without code changes, promoting scalability and maintainability. With that, it simplifies deployment processes and facilitates centralized configuration management. Moreover, environment variables enable separation of configuration from code, aiding in secure and collaborative development. @@ -349,7 +284,7 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc [Short RPC Client]: #connecting-to-the-service-via-python-rpc-client [Customizing Web Interface]: #customizing-the-web-interface [Task Management]: https://pydase.readthedocs.io/en/stable/user-guide/Tasks/ -[Units]: #understanding-units-in-pydase +[Units]: https://pydase.readthedocs.io/en/stable/user-guide/Understanding-Units/ [Property Validation]: https://pydase.readthedocs.io/en/stable/user-guide/Validating-Property-Setters/ [Custom Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/#custom-components-pydasecomponents [Components]: https://pydase.readthedocs.io/en/stable/user-guide/Components/ diff --git a/docs/index.md b/docs/index.md index fd8f105..f19714d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,5 +11,5 @@ [Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client [Customizing Web Interface]: ./user-guide/interaction/main.md#customization-options [Task Management]: ./user-guide/Tasks.md -[Units]: ./getting-started.md#understanding-units-in-pydase +[Units]: ./user-guide/Understanding-Units.md [Property Validation]: ./user-guide/Validating-Property-Setters.md diff --git a/docs/user-guide/Understanding-Units.md b/docs/user-guide/Understanding-Units.md new file mode 100644 index 0000000..052de24 --- /dev/null +++ b/docs/user-guide/Understanding-Units.md @@ -0,0 +1,64 @@ +# Understanding Units + +`pydase` integrates with the [`pint`](https://pint.readthedocs.io/en/stable/) package to allow you to work with physical quantities within your service. This enables you to define attributes with units, making your service more expressive and ensuring consistency in the handling of physical quantities. + +You can define quantities in your `pydase.DataService` subclass using the `pydase.units` module. +Here's an example: + +```python +from typing import Any + +import pydase +import pydase.units as u + + +class ServiceClass(pydase.DataService): + voltage = 1.0 * u.units.V + _current: u.Quantity = 1.0 * u.units.mA + + @property + def current(self) -> u.Quantity: + return self._current + + @current.setter + def current(self, value: u.Quantity) -> None: + self._current = value + + +if __name__ == "__main__": + service = ServiceClass() + + service.voltage = 10.0 * u.units.V + service.current = 1.5 * u.units.mA + + pydase.Server(service=service).run() +``` + +In the frontend, quantities are rendered as floats, with the unit displayed as additional text. This allows you to maintain a clear and consistent representation of physical quantities across both the backend and frontend of your service. +![Web interface with rendered units](../images/Units_App.png) + +Should you need to access the magnitude or the unit of a quantity, you can use the `.m` attribute or the `.u` attribute of the variable, respectively. For example, this could be necessary to set the periodicity of a task: + +```python +import asyncio +import pydase +import pydase.units as u + + +class ServiceClass(pydase.DataService): + readout_wait_time = 1.0 * u.units.ms + + async def read_sensor_data(self): + while True: + print("Reading out sensor ...") + await asyncio.sleep(self.readout_wait_time.to("s").m) + + +if __name__ == "__main__": + service = ServiceClass() + + pydase.Server(service=service).run() +``` + +For more information about what you can do with the units, please consult the documentation of [`pint`](https://pint.readthedocs.io/en/stable/). + diff --git a/mkdocs.yml b/mkdocs.yml index 736ea57..84a6fbc 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,6 +9,7 @@ nav: - Interacting with pydase Services: user-guide/interaction/main.md - Achieving Service Persistence: user-guide/Service_Persistence.md - Understanding Tasks: user-guide/Tasks.md + - Understanding Units: user-guide/Understanding-Units.md - Validating Property Setters: user-guide/Validating-Property-Setters.md - Developer Guide: - Developer Guide: dev-guide/README.md From 7ae3ff504d42dba8c313ac190421abaeda29fdb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 15:59:34 +0200 Subject: [PATCH 10/11] reference link to license --- README.md | 5 +++-- docs/index.md | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 66f7a11..ed37103 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Version](https://img.shields.io/pypi/v/pydase?style=flat)](https://pypi.org/project/pydase/) [![Python Versions](https://img.shields.io/pypi/pyversions/pydase)](https://pypi.org/project/pydase/) [![Documentation Status](https://readthedocs.org/projects/pydase/badge/?version=stable)](https://pydase.readthedocs.io/en/stable/) -[![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)](https://github.com/tiqi-group/pydase/blob/main/LICENSE) +[![License: MIT](https://img.shields.io/github/license/tiqi-group/pydase)][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](https://python-socketio.readthedocs.io/en/stable/) 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][Observer Pattern] to provide the real-time updates, ensuring that changes to the class attributes are reflected across all clients. @@ -275,8 +275,9 @@ We welcome contributions! Please see [contributing.md](https://pydase.readthedoc ## License -`pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE). +`pydase` is licensed under the [MIT License][License]. +[License]: ./LICENSE [Observer Pattern]: https://pydase.readthedocs.io/en/docs/dev-guide/Observer_Pattern_Implementation/ [Service Persistence]: https://pydase.readthedocs.io/en/stable/user-guide/Service_Persistence [Defining DataService]: #defining-a-dataService diff --git a/docs/index.md b/docs/index.md index f19714d..9b81dd3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,6 +4,7 @@ end="" %} +[License]: ./about/license.md [Observer Pattern]: ./dev-guide/Observer_Pattern_Implementation.md [Service Persistence]: ./user-guide/Service_Persistence.md [Defining DataService]: ./getting-started.md#defining-a-dataservice From b2b3d426edd8488585c2220342df1fe751e74b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Mon, 19 Aug 2024 16:11:26 +0200 Subject: [PATCH 11/11] updates license --- LICENSE | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index 3aa9054..c5d8670 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,4 @@ -MIT License - -Copyright (c) 2023 Mose Müller +Copyright (c) 2023-2024 Mose Müller Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal