13 Commits

Author SHA1 Message Date
Mose Müller
1c24ad879c adding docs badge to readme 2023-09-19 18:17:19 +02:00
Mose Müller
6f6cf8dd0d feat: adds readthedocs config file 2023-09-19 18:17:19 +02:00
Mose Müller
e602319f22 docs: updating Readme 2023-09-19 18:17:19 +02:00
Mose Müller
5e16eb9321 docs: adds section explaining how to set the log level 2023-09-19 18:17:19 +02:00
Mose Müller
8ad7ea511c Update Adding_Components.md 2023-09-19 18:17:19 +02:00
Mose Müller
9fa4333196 update Readme structure 2023-09-19 18:17:19 +02:00
Mose Müller
84d4c9c712 Update component section in readme 2023-09-19 18:17:19 +02:00
Mose Müller
b89644864c adds components section to readme 2023-09-19 18:17:19 +02:00
Mose Müller
70bfad6b0a docs: update Adding Components section 2023-09-19 18:17:19 +02:00
Mose Müller
86024be77e docs: adding "Adding Components" section to dev guide 2023-09-19 18:17:19 +02:00
Mose Müller
efac1e790f Updating mkdocs config 2023-09-19 18:17:19 +02:00
Mose Müller
da28b6c82c Adding mkdocs config and docs folder 2023-09-19 18:17:19 +02:00
Mose Müller
0e8970f0c5 Adding docs packages 2023-09-19 18:17:19 +02:00
32 changed files with 1371 additions and 696 deletions

View File

@@ -1,8 +1,8 @@
[flake8]
ignore = E501,W503,FS003,F403,F405,E203,UNT001
ignore = E501,W503,FS003,F403,F405,E203
include = src
max-line-length = 88
max-doc-length = 88
max-complexity = 7
max-expression-complexity = 5.5
use_class_attributes_order_strict_mode=True
use_class_attributes_order_strict_mode=True

View File

@@ -5,36 +5,37 @@ name: Python package
on:
push:
branches: ['main']
branches: [ "main" ]
pull_request:
branches: ['main']
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11']
python-version: ["3.10", "3.11"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
poetry install
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
poetry run flake8 src/pydase --count --show-source --statistics
- name: Test with pytest
run: |
poetry run pytest
- name: Test with pyright
run: |
poetry run pyright src/pydase
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install poetry
poetry install
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
poetry run flake8 src/pydase --count --show-source --statistics
- name: Test with pytest
run: |
poetry run pytest
- name: Test with pyright
run: |
poetry run pyright src/pydase

21
.readthedocs.yaml Normal file
View File

@@ -0,0 +1,21 @@
# Read the Docs configuration file for MkDocs projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
mkdocs:
configuration: mkdocs.yml
# Optionally declare the Python requirements required to build your docs
python:
install:
- method: pip
path: .
- requirements: docs/requirements.txt

171
README.md
View File

@@ -1,5 +1,8 @@
# pydase (Python Data Service) <!-- omit from toc -->
[![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)
`pydase` is a Python library for creating data service servers with integrated web and RPC servers. It's designed to handle the management of data structures, automated tasks, and callbacks, and provides built-in functionality for serving data over different protocols.
- [Features](#features)
@@ -9,9 +12,16 @@
- [Running the Server](#running-the-server)
- [Accessing the Web Interface](#accessing-the-web-interface)
- [Connecting to the Service using rpyc](#connecting-to-the-service-using-rpyc)
- [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)
- [Extending with New Components](#extending-with-new-components)
- [Understanding Service Persistence](#understanding-service-persistence)
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
- [Understanding Units in pydase](#understanding-units-in-pydase)
- [Changing the Log Level](#changing-the-log-level)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)
@@ -19,16 +29,18 @@
## Features
<!-- no toc -->
* [Simple data service definition through class-based interface](#defining-a-dataService)
* [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
* [Support for `rpyc` connections, allowing for programmatic control and interaction with your service](#connecting-to-the-service-using-rpyc)
* [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
* [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)
* [Support for units](#understanding-units-in-pydase)
* Event-based callback functionality for real-time updates
* Support for additional servers for specific use-cases
<!-- * Event-based callback functionality for real-time updates
* Support for additional servers for specific use-cases -->
## Installation
<!--installation-start-->
Install pydase using [`poetry`](https://python-poetry.org/):
```bash
@@ -40,9 +52,10 @@ or `pip`:
```bash
pip install git+https://github.com/tiqi-group/pydase.git
```
<!--installation-end-->
## Usage
<!--usage-start-->
Using `pydase` involves three main steps: defining a `DataService` subclass, running the server, and then connecting to the service either programmatically using `rpyc` or through the web interface.
### Defining a DataService
@@ -143,6 +156,133 @@ print(client.voltage) # prints 5.0
```
In this example, replace `<ip_addr>` with the IP address of the machine where the service is running. After establishing a connection, you can interact with the service attributes as if they were local attributes.
<!--usage-end-->
## 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.
- `enum.Enum`: Presented as an `EnumComponent`, facilitating dropdown selection.
### Method Components
Methods within the `DataService` class have frontend representations:
- Regular Methods: These are rendered as a `MethodComponent` in the frontend, allowing users to execute the method via an "execute" button.
- Asynchronous Methods: These are manifested as the `AsyncMethodComponent` with "start"/"stop" buttons to manage the execution of [tasks](#understanding-tasks-in-pydase).
### 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:
self._channel_id = channel_id
self._current = 0.0
super().__init__()
@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:
self.channels = [Channel(i) for i in range(2)]
super().__init__()
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:
- `Image`: This component allows users to display and update images within the application.
```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`: An interactive slider component to adjust numerical values, including floats and integers, on the frontend while synchronizing the data with the backend in real-time.
```python
import pydase
from pydase.components import NumberSlider
class MyService(pydase.DataService):
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1)
if __name__ == "__main__":
service = MyService()
pydase.Server(service).run()
```
![Slider Component](docs/images/Slider_component.png)
#### Extending with New Components
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](./docs/dev-guide/Adding_Components.md) provides detailed steps on achieving this.
## Understanding Service Persistence
@@ -279,13 +419,34 @@ 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/).
## Changing the Log Level
You can change the log level of loguru by either
1. (RECOMMENDED) setting the `ENVIRONMENT` environment variable to "production" or "development"
```bash
ENVIRONMENT="production" python -m <module_using_pydase>
```
The production environment will only log messages above "INFO", the development environment (default) logs everything above "DEBUG".
2. calling the `pydase.utils.logging.setup_logging` function with the desired log level
```python
# <your_script.py>
from pydase.utils.logging import setup_logging
setup_logging("INFO")
```
## 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](URL_TO_YOUR_DOCUMENTATION) for more information.
## Contributing
We welcome contributions! Please see [CONTRIBUTING.md](URL_TO_YOUR_CONTRIBUTING_GUIDELINES) for details on how to contribute.
We welcome contributions! Please see [contributing.md](./docs/about/contributing.md) for details on how to contribute.
## License

View File

10
docs/about/license.md Normal file
View File

@@ -0,0 +1,10 @@
License
=======
`pydase` is released under the *MIT license*:
```{.license}
{%
include "../../LICENSE"
comments=false
%}
```

View File

3
docs/css/extra.css Normal file
View File

@@ -0,0 +1,3 @@
.language-license{
background-color: #eeffcc !important;
}

View File

@@ -0,0 +1,299 @@
# Adding Components to `pydase`
This guide provides a step-by-step process for adding new components to the `pydase` package. Components in `pydase` consist of both backend (Python) and frontend (React) parts. They work together to create interactive and dynamic data services.
## Overview
A component in `pydase` is a unique combination of a backend class (e.g., `Image`) and its corresponding frontend React component. The backend class stores the attributes needed for the component, and possibly methods for setting those in the backend, while the frontend part is responsible for rendering and interacting with the component.
## Adding a Backend Component to `pydase`
Backend components belong in the `src/pydase/components` directory.
### Step 1: Create a New Python File in the Components Directory
Navigate to the `src/pydase/components` directory and create a new Python file for your component. The name of the file should be descriptive of the component's functionality.
For example, for a `Image` component, create a file named `image.py`.
### Step 2: Define the Backend Class
Within the newly created file, define a Python class representing the component. This class should inherit from `DataService` and contains the attributes that the frontend needs to render the component. Every public attribute defined in this class will synchronise across the clients. It can also contain methods which can be used to interact with the component from the backend.
For the `Image` component, the class may look like this:
```python
# file: pydase/components/image.py
from pydase.data_service.data_service import DataService
class Image(DataService):
def __init__(
self,
image_representation: bytes = b"",
) -> None:
self.image_representation = image_representation
super().__init__()
# need to decode the bytes
def __setattr__(self, __name: str, __value: Any) -> None:
if __name == "value":
if isinstance(__value, bytes):
__value = __value.decode()
return super().__setattr__(__name, __value)
```
So, changing the `image_representation` will push the updated value to the browsers connected to the service.
### Step 3: Register the Backend Class
The component should be added to the `__init__.py` file to ensure `pydase` handles them properly:
```python
# file: pydase/components/__init__.py
from pydase.components.image import Image
from pydase.components.number_slider import NumberSlider
__all__ = [
"NumberSlider",
"Image", # add the new components here
]
```
### Step 4: Implement Necessary Methods (Optional)
If your component requires specific logic or methods, implement them within the class. Document any public methods or attributes to ensure that other developers understand their purpose and usage.
### Step 5: Write Tests for the Component (Recommended)
Consider writing unit tests for the component to verify its behavior. Place the tests in the appropriate directory within the `tests` folder.
For example, a test for the `Image` component could look like this:
```python
from pytest import CaptureFixture
from pydase.components.image import Image
from pydase.data_service.data_service import DataService
def test_Image(capsys: CaptureFixture) -> None:
class ServiceClass(DataService):
image = Image()
service = ServiceClass()
# ...
```
## Adding a Frontend Component to `pydase`
Frontend components in `pydase` live in the `frontend/src/components/` directory. Follow these steps to create and add a new frontend component:
### Step 1: Create a New React Component File in the Components Directory
Navigate to the `frontend/src/components/` directory and create a new React component file for your component. The name of the file should be descriptive of the component's functionality and reflect the naming conventions used in your project.
For example, for an `Image` component, create a file named `ImageComponent.tsx`.
### Step 2: Write the React Component Code
Write the React component code, following the structure and patterns used in existing components. Make sure to import necessary libraries and dependencies.
For example, for the `Image` component, a template could look like this:
```tsx
import { emit_update } from '../socket'; // use this when your component should update values in the backend
import { DocStringComponent } from './DocStringComponent';
import React, { useEffect, useRef, useState } from 'react';
import { Card, Collapse, Image } from 'react-bootstrap';
import { DocStringComponent } from './DocStringComponent';
import { ChevronDown, ChevronRight } from 'react-bootstrap-icons';
interface ImageComponentProps {
name: string;
parentPath: string;
readOnly: boolean;
docString: string;
addNotification: (string) => void;
// Define your component specific props here
value: string;
format: string;
}
export const ImageComponent = React.memo((props: ImageComponentProps) => {
const { name, parentPath, value, docString, format, addNotification } = props;
const renderCount = useRef(0);
const [open, setOpen] = useState(true); // add this if you want to expand/collapse your component
useEffect(() => {
renderCount.current++;
});
// This will trigger a notification if notifications are enabled.
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
}, [props.value]);
// Your component logic here
return (
<div className={'imageComponent'} id={parentPath.concat('.' + name)}>
{/* Add the Card and Collapse components here if you want to be able to expand and
collapse your component. */}
<Card>
<Card.Header
onClick={() => setOpen(!open)}
style={{ cursor: 'pointer' }} // Change cursor style on hover
>
{name} {open ? <ChevronDown /> : <ChevronRight />}
</Card.Header>
<Collapse in={open}>
<Card.Body>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}
<DocStringComponent docString={docString} />
{/* Your component TSX here */}
</Card.Body>
</Collapse>
</Card>
</div>
);
});
```
### Step 3: Emitting Updates to the Backend
Often, React components in the frontend will need to send updates to the backend, especially when user interactions result in a change of state or data. In `pydase`, we use `socketio` to seamlessly communicate these changes. Here's a detailed guide on how to emit update events from your frontend component:
1. **Setting Up Emission**: Ensure you've imported the required functions and methods for emission. The main function we'll use for this is `emit_update` from the `socket` module:
```tsx
import { emit_update } from '../socket';
```
2. **Understanding the Emission Parameters**:
When emitting an update, we send three main pieces of data:
- `parentPath`: This is the access path for the parent object of the attribute to be updated. This forms the basis to create the full access path for the attribute. For instance, for the attribute access path `attr1.list_attr[0].attr2`, `attr1.list_attr[0]` would be the `parentPath`.
- `name`: This represents the name of the attribute to be updated within the `DataService` instance. If the attribute is part of a nested structure, this would be the name of the attribute in the last nested object. So, for `attr1.list_attr[0].attr2`, `attr2` would be the name.
- `value`: This is the new value intended for the attribute. Ensure that the type of this value matches the type of the attribute in the backend.
3. **Implementing the Emission**:
To illustrate the emission process, let's consider the `ButtonComponent`. When the button state changes, we want to send this update to the backend:
```tsx
// ... (other imports)
export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
// ...
const { name, parentPath, value } = props;
const setChecked = (checked: boolean) => {
emit_update(name, parentPath, checked);
};
return (
<ToggleButton
checked={value}
value={parentPath}
// ... other props
onChange={(e) => setChecked(e.currentTarget.checked)}>
<p>{name}</p>
</ToggleButton>
);
});
```
In this example, whenever the button's checked state changes (`onChange` event), we invoke the `setChecked` method, which in turn emits the new state to the backend using `emit_update`.
### Step 4: Add the New Component to the GenericComponent
The `GenericComponent` is responsible for rendering different types of components based on the attribute type. You can add the new `ImageComponent` to the `GenericComponent` by following these sub-steps:
#### 1. Import the New Component
At the beginning of the `GenericComponent` file, import the newly created `ImageComponent`:
```tsx
// file: frontend/src/components/GenericComponent.tsx
import { ImageComponent } from './ImageComponent';
```
#### 2. Update the AttributeType
Update the `AttributeType` type definition to include the new type for the `ImageComponent`.
For example, if the new attribute type is `'Image'` (which should correspond to the name of the backend component class), you can add it to the union:
```tsx
type AttributeType =
| 'str'
| 'bool'
| 'float'
| 'int'
| 'Quantity'
| 'list'
| 'method'
| 'DataService'
| 'Enum'
| 'NumberSlider'
| 'Image'; // Add the name of the backend component class here
```
#### 3. Add a Conditional Branch for the New Component
Inside the `GenericComponent` function, add a new conditional branch to render the `ImageComponent` when the attribute type is `'Image'`:
```tsx
} else if (attribute.type === 'Image') {
return (
<ImageComponent
name={name}
parentPath={parentPath}
readOnly={attribute.readonly}
docString={attribute.doc}
addNotification={addNotification}
// Add any other specific props for the ImageComponent here
value={attribute.value['value']['value'] as string}
format={attribute.value['format']['value'] as string}
/>
);
} else {
// other code
```
Make sure to update the props passed to the `ImageComponent` based on its specific requirements.
### Step 5: Adding Custom Notification Message (Optional)
In some cases, you may want to provide a custom notification message to the user when an attribute of a specific type is updated. This can be useful for enhancing user experience and providing contextual information about the changes.
For example, updating an `Image` component corresponds to setting a very long string. We don't want to display the whole string in the notification but just notify the user that the image was updated (and maybe also the format).
To create a custom notification message, you can update the message passed to the `addNotification` method in the `useEffect` hook in the component file file. For the `ImageComponent`, this could look like this:
```tsx
useEffect(() => {
addNotification(`${parentPath}.${name} changed.`);
}, [props.value]);
```
However, you might want to use the `addNotification` at different places. For an example, see the [MethodComponent](../../frontend/src/components/MethodComponent.tsx).
### Step 6: Write Tests for the Component (TODO)
Test the frontend component to ensure that it renders correctly and interacts seamlessly
with the backend. Consider writing unit tests using a testing library like Jest or React
Testing Library, and manually test the component in the browser.

8
docs/dev-guide/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Developer Guide
Extending `pydase`.
---
- [API Reference](api.md)
- [Adding Components](Adding_Components.md)

0
docs/dev-guide/api.md Normal file
View File

14
docs/getting-started.md Normal file
View File

@@ -0,0 +1,14 @@
# Getting Started
## Installation
{%
include-markdown "../README.md"
start="<!--installation-start-->"
end="<!--installation-end-->"
%}
## Usage
{%
include-markdown "../README.md"
start="<!--usage-start-->"
end="<!--usage-end-->"
%}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

1
docs/index.md Normal file
View File

@@ -0,0 +1 @@
{% include-markdown "../README.md" %}

20
docs/requirements.txt Normal file
View File

@@ -0,0 +1,20 @@
click==8.1.7 ; python_version >= "3.10" and python_version < "4.0"
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" and platform_system == "Windows"
ghp-import==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0"
markdown==3.4.4 ; python_version >= "3.10" and python_version < "4.0"
markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0"
mergedeep==1.3.4 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-autorefs==0.5.0 ; python_version >= "3.10" and python_version < "4.0"
mkdocs-include-markdown-plugin==3.9.1 ; python_version >= "3.10" and python_version < "4.0"
mkdocs==1.5.3 ; python_version >= "3.10" and python_version < "4.0"
mkdocstrings==0.22.0 ; python_version >= "3.10" and python_version < "4.0"
packaging==23.1 ; python_version >= "3.10" and python_version < "4.0"
pathspec==0.11.2 ; python_version >= "3.10" and python_version < "4.0"
platformdirs==3.10.0 ; python_version >= "3.10" and python_version < "4.0"
pymdown-extensions==10.3 ; python_version >= "3.10" and python_version < "4.0"
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
pyyaml-env-tag==0.1 ; python_version >= "3.10" and python_version < "4.0"
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
watchdog==3.0.0 ; python_version >= "3.10" and python_version < "4.0"

42
mkdocs.yml Normal file
View File

@@ -0,0 +1,42 @@
site_name: pydase Documentation
repo_url: https://github.com/tiqi-group/pydase
edit_uri: blob/docs/docs/
nav:
- Home: index.md
- Getting Started: getting-started.md
- Developer Guide:
- Developer Guide: dev-guide/README.md
- API Reference: dev-guide/api.md
- Adding Components: dev-guide/Adding_Components.md
- About:
- Release Notes: about/release-notes.md
- Contributing: about/contributing.md
- License: about/license.md
theme: readthedocs
extra_css:
- css/extra.css
markdown_extensions:
- smarty
- toc:
permalink: true
baselevel: 4
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.snippets
- pymdownx.superfences
# - pymdownx.highlight:
# - pymdownx.inlinehilite
plugins:
- include-markdown
- search
- mkdocstrings
watch:
- src/pydase

1333
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,2 @@
[virtualenvs]
in-project = true
prefer-active-python = true

View File

@@ -8,7 +8,7 @@ packages = [{ include = "pydase", from = "src" }]
[tool.poetry.dependencies]
python = "^3.9"
python = "^3.10"
rpyc = "^5.3.1"
loguru = "^0.7.0"
fastapi = "^0.100.0"
@@ -37,6 +37,13 @@ flake8-eradicate = "^1.4.0"
matplotlib = "^3.7.2"
pyright = "^1.1.323"
[tool.poetry.group.docs.dependencies]
mkdocs = "^1.5.2"
mkdocs-include-markdown-plugin = "^3.9.1"
mkdocstrings = "^0.22.0"
pymdown-extensions = "^10.1"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -1,7 +1,7 @@
import base64
import io
from pathlib import Path
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING, Optional
from urllib.request import urlopen
import PIL.Image
@@ -29,7 +29,7 @@ class Image(DataService):
def format(self) -> str:
return self._format
def load_from_path(self, path: Union[Path, str]) -> None:
def load_from_path(self, path: Path | str) -> None:
with PIL.Image.open(path) as image:
self._load_from_PIL(image)
@@ -68,7 +68,7 @@ class Image(DataService):
else:
logger.error("Image format is 'None'. Skipping...")
def _get_image_format_from_bytes(self, value_: bytes) -> Union[str, None]:
def _get_image_format_from_bytes(self, value_: bytes) -> str | None:
image_data = base64.b64decode(value_)
# Create a writable memory buffer for the image
image_buffer = io.BytesIO(image_data)

View File

@@ -1,4 +1,4 @@
from typing import Any, Literal, Union
from typing import Any, Literal
from loguru import logger
@@ -39,11 +39,11 @@ class NumberSlider(DataService):
def __init__(
self,
value: Union[float, int] = 0,
value: float | int = 0,
min: float = 0.0,
max: float = 100.0,
step_size: Union[float, int] = 1.0,
type: Union[Literal["int"], Literal["float"]] = "float",
step_size: float | int = 1.0,
type: Literal["int"] | Literal["float"] = "float",
) -> None:
if type not in {"float", "int"}:
logger.error(f"Unknown type '{type}'. Using 'float'.")

View File

@@ -1,9 +1,9 @@
from typing import Literal, Union
from typing import Literal
from confz import BaseConfig, EnvSource
class OperationMode(BaseConfig): # type: ignore
environment: Union[Literal["development"], Literal["production"]] = "development"
environment: Literal["development"] | Literal["production"] = "development"
CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"])

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import inspect
from collections.abc import Callable
from typing import TYPE_CHECKING, Any, Union
from typing import TYPE_CHECKING, Any
from loguru import logger
@@ -206,8 +206,8 @@ class CallbackManager:
def __register_recursive_parameter_callback(
self,
obj: Union["AbstractDataService", DataServiceList],
callback: Callable[[Union[str, int], Any], None],
obj: "AbstractDataService | DataServiceList",
callback: Callable[[str | int, Any], None],
) -> None:
"""
Register callback to a DataService or DataServiceList instance and its nested
@@ -222,7 +222,7 @@ class CallbackManager:
if isinstance(obj, DataServiceList):
# emits callback when item in list gets reassigned
obj.add_callback(callback=callback)
obj_list: Union[DataServiceList, list[AbstractDataService]] = obj
obj_list: DataServiceList | list[AbstractDataService] = obj
else:
obj_list = [obj]
@@ -337,7 +337,7 @@ class CallbackManager:
# Create and register a callback for the object
# only emit the notification when the call was registered by the root object
callback: Callable[[str, Union[dict[str, Any], None]], None] = (
callback: Callable[[str, dict[str, Any] | None], None] = (
lambda name, status: obj._callback_manager.emit_notification(
parent_path=parent_path, name=name, value=status
)

View File

@@ -1,5 +1,5 @@
from collections.abc import Callable
from typing import Any, Union
from typing import Any
from pydase.utils.warnings import (
warn_if_instance_class_does_not_inherit_from_DataService,
@@ -31,7 +31,7 @@ class DataServiceList(list):
def __init__(
self,
*args: list[Any],
callback: Union[list[Callable[[int, Any], None]], None] = None,
callback: list[Callable[[int, Any], None]] | None = None,
**kwargs: Any,
) -> None:
self.callbacks: list[Callable[[int, Any], None]] = []

View File

@@ -4,7 +4,7 @@ import asyncio
import inspect
from collections.abc import Callable
from functools import wraps
from typing import TYPE_CHECKING, Any, TypedDict, Union
from typing import TYPE_CHECKING, Any, TypedDict
from loguru import logger
@@ -82,7 +82,7 @@ class TaskManager:
"""
self.task_status_change_callbacks: list[
Callable[[str, Union[dict[str, Any], None]], Any]
Callable[[str, dict[str, Any] | None], Any]
] = []
"""A list of callback functions to be invoked when the status of a task (start
or stop) changes."""

View File

@@ -5,7 +5,7 @@ import threading
from concurrent.futures import ThreadPoolExecutor
from enum import Enum
from types import FrameType
from typing import Any, Optional, Protocol, TypedDict, Union
from typing import Any, Optional, Protocol, TypedDict
import uvicorn
from loguru import logger
@@ -180,7 +180,7 @@ class Server:
self._additional_servers = additional_servers
self.should_exit = False
self.servers: dict[str, asyncio.Future[Any]] = {}
self.executor: Union[ThreadPoolExecutor, None] = None
self.executor: ThreadPoolExecutor | None = None
self._info: dict[str, Any] = {
"name": self._service.get_service_name(),
"version": __version__,

View File

@@ -1,5 +1,5 @@
from pathlib import Path
from typing import Any, TypedDict, Union
from typing import Any, TypedDict
import socketio
from fastapi import FastAPI
@@ -47,8 +47,8 @@ class WebAPI:
def __init__( # noqa: CFQ002
self,
service: DataService,
frontend: Union[str, Path, None] = None,
css: Union[str, Path, None] = None,
frontend: str | Path | None = None,
css: str | Path | None = None,
enable_CORS: bool = True,
info: dict[str, Any] = {},
*args: Any,

View File

@@ -1,19 +1,21 @@
from typing import TypedDict, Union
from typing import TypedDict
import pint
from pint import Quantity
units = pint.UnitRegistry()
units: pint.UnitRegistry = pint.UnitRegistry()
units.default_format = "~P" # pretty and short format
Quantity = pint.Quantity
Unit = units.Unit
class QuantityDict(TypedDict):
magnitude: Union[int, float]
magnitude: int | float
unit: str
def convert_to_quantity(
value: Union[QuantityDict, float, int, Quantity], unit: str = ""
value: QuantityDict | float | int | Quantity, unit: str = ""
) -> Quantity:
"""
Convert a given value into a pint.Quantity object with the specified unit.
@@ -45,10 +47,10 @@ def convert_to_quantity(
will be unitless.
"""
if isinstance(value, (int, float)):
quantity = float(value) * units(unit)
if isinstance(value, int | float):
quantity = float(value) * Unit(unit)
elif isinstance(value, dict):
quantity = float(value["magnitude"]) * units(value["unit"])
quantity = float(value["magnitude"]) * Unit(value["unit"])
else:
quantity = value
return quantity # type: ignore

View File

@@ -1,13 +1,13 @@
import re
from itertools import chain
from typing import Any, Optional, Union, cast
from typing import Any, Optional, cast
from loguru import logger
STANDARD_TYPES = ("int", "float", "bool", "str", "Enum", "NoneType", "Quantity")
def get_class_and_instance_attributes(obj: Any) -> dict[str, Any]:
def get_class_and_instance_attributes(obj: object) -> dict[str, Any]:
"""Dictionary containing all attributes (both instance and class level) of a
given object.
@@ -126,9 +126,7 @@ def generate_paths_from_DataService_dict(
return paths
def extract_dict_or_list_entry(
data: dict[str, Any], key: str
) -> Union[dict[str, Any], None]:
def extract_dict_or_list_entry(data: dict[str, Any], key: str) -> dict[str, Any] | None:
"""
Extract a nested dictionary or list entry based on the provided key.
@@ -180,7 +178,7 @@ def extract_dict_or_list_entry(
else:
logger.error(f"Invalid index format in key: {key}")
current_data: Union[dict[str, Any], list[dict[str, Any]], None] = data.get(
current_data: dict[str, Any] | list[dict[str, Any]] | None = data.get(
attr_name, None
)
if not isinstance(current_data, dict):
@@ -253,7 +251,7 @@ def get_nested_value_from_DataService_by_path_and_key(
# Split the path into parts
parts: list[str] = re.split(r"\.", path) # Split by '.'
current_data: Union[dict[str, Any], None] = data
current_data: dict[str, Any] | None = data
for part in parts:
if current_data is None:
@@ -266,7 +264,7 @@ def get_nested_value_from_DataService_by_path_and_key(
def convert_arguments_to_hinted_types(
args: dict[str, Any], type_hints: dict[str, Any]
) -> Union[dict[str, Any], str]:
) -> dict[str, Any] | str:
"""
Convert the given arguments to their types hinted in the type_hints dictionary.
@@ -308,7 +306,7 @@ def convert_arguments_to_hinted_types(
def update_value_if_changed(
target: Any, attr_name_or_index: Union[str, int], new_value: Any
target: Any, attr_name_or_index: str | int, new_value: Any
) -> None:
"""
Updates the value of an attribute or a list element on a target object if the new

View File

@@ -1,7 +1,7 @@
import logging
import sys
from types import FrameType
from typing import Optional, Union
from typing import Optional
import loguru
import rpyc
@@ -21,7 +21,7 @@ class InterceptHandler(logging.Handler):
return
# Get corresponding Loguru level if it exists.
level: Union[int, str]
level: int | str
try:
level = loguru.logger.level(record.levelname).name
except ValueError: