mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-18 20:21:21 +01:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa85f6453f | ||
|
|
343354e0ee | ||
|
|
b38bb05c69 | ||
|
|
a0dab630f9 | ||
|
|
a9db7848f7 | ||
|
|
a8b14180ad | ||
|
|
26a366842a | ||
|
|
b0e7de2d2c | ||
|
|
bbcba8b39f | ||
|
|
34e46e05ee | ||
|
|
93c2f5ab70 | ||
|
|
106ffbfc40 | ||
|
|
5702adbdbd | ||
|
|
e3a7932ac4 | ||
|
|
21cd039610 | ||
|
|
0b5dd5393a | ||
|
|
0f7a8ec63b | ||
|
|
6fe30fc6ec | ||
|
|
5f64ec131c | ||
|
|
06bf5fb539 | ||
|
|
bdbb79e131 | ||
|
|
78b94d4fc8 | ||
|
|
6d12371e7b | ||
|
|
fb6146e01d |
46
.github/workflows/publish-to-pypi.yaml
vendored
46
.github/workflows/publish-to-pypi.yaml
vendored
@@ -86,26 +86,26 @@ jobs:
|
||||
'${{ github.ref_name }}' dist/**
|
||||
--repo '${{ github.repository }}'
|
||||
|
||||
publish-to-testpypi:
|
||||
name: Publish Python 🐍 distribution 📦 to TestPyPI
|
||||
needs:
|
||||
- build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
environment:
|
||||
name: testpypi
|
||||
url: https://test.pypi.org/p/pydase
|
||||
|
||||
permissions:
|
||||
id-token: write # IMPORTANT: mandatory for trusted publishing
|
||||
|
||||
steps:
|
||||
- name: Download all the dists
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: python-package-distributions
|
||||
path: dist/
|
||||
- name: Publish distribution 📦 to TestPyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
# publish-to-testpypi:
|
||||
# name: Publish Python 🐍 distribution 📦 to TestPyPI
|
||||
# needs:
|
||||
# - build
|
||||
# runs-on: ubuntu-latest
|
||||
#
|
||||
# environment:
|
||||
# name: testpypi
|
||||
# url: https://test.pypi.org/p/pydase
|
||||
#
|
||||
# permissions:
|
||||
# id-token: write # IMPORTANT: mandatory for trusted publishing
|
||||
#
|
||||
# steps:
|
||||
# - name: Download all the dists
|
||||
# uses: actions/download-artifact@v3
|
||||
# with:
|
||||
# name: python-package-distributions
|
||||
# path: dist/
|
||||
# - name: Publish distribution 📦 to TestPyPI
|
||||
# uses: pypa/gh-action-pypi-publish@release/v1
|
||||
# with:
|
||||
# repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
138
README.md
138
README.md
@@ -44,13 +44,13 @@
|
||||
Install pydase using [`poetry`](https://python-poetry.org/):
|
||||
|
||||
```bash
|
||||
poetry add git+https://github.com/tiqi-group/pydase.git
|
||||
poetry add pydase
|
||||
```
|
||||
|
||||
or `pip`:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/tiqi-group/pydase.git
|
||||
pip install pydase
|
||||
```
|
||||
<!--installation-end-->
|
||||
|
||||
@@ -129,7 +129,7 @@ if __name__ == "__main__":
|
||||
Server(service).run()
|
||||
```
|
||||
|
||||
This will start the server, making your Device service accessible via RPC and a web server at http://localhost:8001.
|
||||
This will start the server, making your Device service accessible via RPC and a web server at [http://localhost:8001](http://localhost:8001).
|
||||
|
||||
### Accessing the Web Interface
|
||||
|
||||
@@ -159,11 +159,15 @@ In this example, replace `<ip_addr>` with the IP address of the machine where th
|
||||
<!--usage-end-->
|
||||
|
||||
## Understanding the Component System
|
||||
|
||||
<!-- Component User Guide Start -->
|
||||
|
||||
In `pydase`, components are fundamental building blocks that bridge the Python backend logic with frontend visual representation and interactions. This system can be understood based on the following categories:
|
||||
|
||||
### Built-in Type and Enum Components
|
||||
|
||||
`pydase` automatically maps standard Python data types to their corresponding frontend components:
|
||||
|
||||
- `str`: Translated into a `StringComponent` on the frontend.
|
||||
- `int` and `float`: Manifested as the `NumberComponent`.
|
||||
- `bool`: Rendered as a `ButtonComponent`.
|
||||
@@ -173,6 +177,7 @@ In `pydase`, components are fundamental building blocks that bridge the Python b
|
||||
### Method Components
|
||||
|
||||
Methods within the `DataService` class have frontend representations:
|
||||
|
||||
- Regular Methods: These are rendered as a `MethodComponent` in the frontend, allowing users to execute the method via an "execute" button.
|
||||
- Asynchronous Methods: These are manifested as the `AsyncMethodComponent` with "start"/"stop" buttons to manage the execution of [tasks](#understanding-tasks-in-pydase).
|
||||
|
||||
@@ -228,61 +233,111 @@ The custom components in `pydase` have two main parts:
|
||||
|
||||
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
|
||||
|
||||
- `Image`: This component allows users to display and update images within the application.
|
||||
#### `Image`
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
This component provides a versatile interface for displaying images within the application. Users can update and manage images from various sources, including local paths, URLs, and even matplotlib figures.
|
||||
|
||||
import pydase
|
||||
from pydase.components.image import Image
|
||||
The component offers methods to load images seamlessly, ensuring that visual content is easily integrated and displayed within the data service.
|
||||
|
||||
```python
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
import pydase
|
||||
from pydase.components.image import Image
|
||||
|
||||
|
||||
class MyDataService(pydase.DataService):
|
||||
my_image = Image()
|
||||
class MyDataService(pydase.DataService):
|
||||
my_image = Image()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
service = MyDataService()
|
||||
# loading from local path
|
||||
service.my_image.load_from_path("/your/image/path/")
|
||||
if __name__ == "__main__":
|
||||
service = MyDataService()
|
||||
# loading from local path
|
||||
service.my_image.load_from_path("/your/image/path/")
|
||||
|
||||
# loading from a URL
|
||||
service.my_image.load_from_url("https://cataas.com/cat")
|
||||
# loading from a URL
|
||||
service.my_image.load_from_url("https://cataas.com/cat")
|
||||
|
||||
# loading a matplotlib figure
|
||||
fig = plt.figure()
|
||||
x = np.linspace(0, 2 * np.pi)
|
||||
plt.plot(x, np.sin(x))
|
||||
plt.grid()
|
||||
service.my_image.load_from_matplotlib_figure(fig)
|
||||
# loading a matplotlib figure
|
||||
fig = plt.figure()
|
||||
x = np.linspace(0, 2 * np.pi)
|
||||
plt.plot(x, np.sin(x))
|
||||
plt.grid()
|
||||
service.my_image.load_from_matplotlib_figure(fig)
|
||||
|
||||
pydase.Server(service).run()
|
||||
```
|
||||
pydase.Server(service).run()
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
- `NumberSlider`: An interactive slider component to adjust numerical values, including floats and integers, on the frontend while synchronizing the data with the backend in real-time.
|
||||
#### `NumberSlider`
|
||||
|
||||
```python
|
||||
import pydase
|
||||
from pydase.components import NumberSlider
|
||||
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
|
||||
|
||||
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
|
||||
|
||||
```python
|
||||
import pydase
|
||||
from pydase.components import NumberSlider
|
||||
|
||||
|
||||
class MyService(pydase.DataService):
|
||||
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1)
|
||||
class MyService(pydase.DataService):
|
||||
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
service = MyService()
|
||||
pydase.Server(service).run()
|
||||
```
|
||||
if __name__ == "__main__":
|
||||
service = MyService()
|
||||
pydase.Server(service).run()
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
#### `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
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](https://pydase.readthedocs.io/en/latest/dev-guide/Adding_Components/) provides detailed steps on achieving this.
|
||||
|
||||
<!-- Component User Guide End -->
|
||||
|
||||
## Understanding Service Persistence
|
||||
|
||||
@@ -422,6 +477,7 @@ For more information about what you can do with the units, please consult the do
|
||||
## 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
|
||||
@@ -442,12 +498,12 @@ You can change the log level of loguru by either
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions! Please see [contributing.md](./docs/about/contributing.md) for details on how to contribute.
|
||||
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/latest/about/contributing/) for details on how to contribute.
|
||||
|
||||
## License
|
||||
|
||||
`pydase` is licensed under the [MIT License](./LICENSE).
|
||||
`pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE).
|
||||
|
||||
BIN
docs/images/ColouredEnum_component.png
Normal file
BIN
docs/images/ColouredEnum_component.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
7
docs/user-guide/Components.md
Normal file
7
docs/user-guide/Components.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Components Guide
|
||||
{%
|
||||
include-markdown "../../README.md"
|
||||
start="<!-- Component User Guide Start -->"
|
||||
end="<!-- Component User Guide End -->"
|
||||
heading-offset=-2
|
||||
%}
|
||||
75
frontend/src/components/ColouredEnumComponent.tsx
Normal file
75
frontend/src/components/ColouredEnumComponent.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
|
||||
import { emit_update } from '../socket';
|
||||
import { DocStringComponent } from './DocStringComponent';
|
||||
|
||||
interface ColouredEnumComponentProps {
|
||||
name: string;
|
||||
parentPath: string;
|
||||
value: string;
|
||||
docString?: string;
|
||||
readOnly: boolean;
|
||||
enumDict: Record<string, string>;
|
||||
addNotification: (string) => void;
|
||||
}
|
||||
|
||||
export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => {
|
||||
const {
|
||||
name,
|
||||
parentPath: parentPath,
|
||||
value,
|
||||
docString,
|
||||
enumDict,
|
||||
readOnly,
|
||||
addNotification
|
||||
} = props;
|
||||
const renderCount = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
renderCount.current++;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
addNotification(`${parentPath}.${name} changed to ${value}.`);
|
||||
}, [props.value]);
|
||||
|
||||
const handleValueChange = (newValue) => {
|
||||
console.log(newValue);
|
||||
emit_update(name, parentPath, newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
|
||||
{process.env.NODE_ENV === 'development' && (
|
||||
<p>Render count: {renderCount.current}</p>
|
||||
)}
|
||||
<DocStringComponent docString={docString} />
|
||||
<Row>
|
||||
<Col className="d-flex align-items-center">
|
||||
<InputGroup.Text>{name}</InputGroup.Text>
|
||||
{readOnly ? (
|
||||
// Display the Form.Control when readOnly is true
|
||||
<Form.Control
|
||||
value={value}
|
||||
disabled={true}
|
||||
style={{ backgroundColor: enumDict[value] }}
|
||||
/>
|
||||
) : (
|
||||
// Display the Form.Select when readOnly is false
|
||||
<Form.Select
|
||||
aria-label="coloured-enum-select"
|
||||
value={value}
|
||||
style={{ backgroundColor: enumDict[value] }}
|
||||
onChange={(event) => handleValueChange(event.target.value)}>
|
||||
{Object.entries(enumDict).map(([key, val]) => (
|
||||
<option key={key} value={key}>
|
||||
{key}
|
||||
</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { StringComponent } from './StringComponent';
|
||||
import { ListComponent } from './ListComponent';
|
||||
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
|
||||
import { ImageComponent } from './ImageComponent';
|
||||
import { ColouredEnumComponent } from './ColouredEnumComponent';
|
||||
|
||||
type AttributeType =
|
||||
| 'str'
|
||||
@@ -21,7 +22,8 @@ type AttributeType =
|
||||
| 'DataService'
|
||||
| 'Enum'
|
||||
| 'NumberSlider'
|
||||
| 'Image';
|
||||
| 'Image'
|
||||
| 'ColouredEnum';
|
||||
|
||||
type ValueType = boolean | string | number | object;
|
||||
export interface Attribute {
|
||||
@@ -181,6 +183,19 @@ export const GenericComponent = React.memo(
|
||||
addNotification={addNotification}
|
||||
/>
|
||||
);
|
||||
} else if (attribute.type === 'ColouredEnum') {
|
||||
console.log(attribute);
|
||||
return (
|
||||
<ColouredEnumComponent
|
||||
name={name}
|
||||
parentPath={parentPath}
|
||||
docString={attribute.doc}
|
||||
value={String(attribute.value)}
|
||||
readOnly={attribute.readonly}
|
||||
enumDict={attribute.enum}
|
||||
addNotification={addNotification}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <div key={name}>{name}</div>;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ edit_uri: blob/docs/docs/
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Getting Started: getting-started.md
|
||||
- User Guide:
|
||||
- Components Guide: user-guide/Components.md
|
||||
- Developer Guide:
|
||||
- Developer Guide: dev-guide/README.md
|
||||
- API Reference: dev-guide/api.md
|
||||
@@ -22,7 +24,6 @@ markdown_extensions:
|
||||
- smarty
|
||||
- toc:
|
||||
permalink: true
|
||||
baselevel: 4
|
||||
- pymdownx.highlight:
|
||||
anchor_linenums: true
|
||||
- pymdownx.snippets
|
||||
@@ -38,5 +39,3 @@ plugins:
|
||||
|
||||
watch:
|
||||
- src/pydase
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pydase"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases."
|
||||
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
|
||||
readme = "README.md"
|
||||
|
||||
@@ -27,10 +27,12 @@ print(my_service.voltage.value) # Output: 5
|
||||
```
|
||||
"""
|
||||
|
||||
from pydase.components.coloured_enum import ColouredEnum
|
||||
from pydase.components.image import Image
|
||||
from pydase.components.number_slider import NumberSlider
|
||||
|
||||
__all__ = [
|
||||
"NumberSlider",
|
||||
"Image",
|
||||
"ColouredEnum",
|
||||
]
|
||||
|
||||
61
src/pydase/components/coloured_enum.py
Normal file
61
src/pydase/components/coloured_enum.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ColouredEnum(Enum):
|
||||
"""
|
||||
Represents a UI element that can display colour-coded text based on its value.
|
||||
|
||||
This class extends the standard Enum but requires its values to be valid CSS
|
||||
colour codes. Supported colour formats include:
|
||||
- Hexadecimal colours
|
||||
- Hexadecimal colours with transparency
|
||||
- RGB colours
|
||||
- RGBA colours
|
||||
- HSL colours
|
||||
- HSLA colours
|
||||
- Predefined/Cross-browser colour names
|
||||
Refer to the this website for more details on colour formats:
|
||||
(https://www.w3schools.com/cssref/css_colours_legal.php)
|
||||
|
||||
The behavior of this component in the UI depends on how it's defined in the data
|
||||
service:
|
||||
- As property with a setter or as attribute: Renders as a dropdown menu,
|
||||
allowing users to select and change its value from the frontend.
|
||||
- As property without a setter: Displays as a coloured box with the key of the
|
||||
`ColouredEnum` as text inside, serving as a visual indicator without user
|
||||
interaction.
|
||||
|
||||
Example:
|
||||
--------
|
||||
```python
|
||||
import pydase.components as pyc
|
||||
import pydase
|
||||
|
||||
class MyStatus(pyc.ColouredEnum):
|
||||
PENDING = "#FFA500" # Orange
|
||||
RUNNING = "#0000FF80" # Transparent Blue
|
||||
PAUSED = "rgb(169, 169, 169)" # Dark Gray
|
||||
RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow
|
||||
COMPLETED = "hsl(120, 100%, 50%)" # Green
|
||||
FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red
|
||||
CANCELLED = "SlateGray" # Slate Gray
|
||||
|
||||
class StatusExample(pydase.DataService):
|
||||
_status = MyStatus.RUNNING
|
||||
|
||||
@property
|
||||
def status(self) -> MyStatus:
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, value: MyStatus) -> None:
|
||||
# Custom logic here...
|
||||
self._status = value
|
||||
|
||||
# Example usage:
|
||||
my_service = StatusExample()
|
||||
my_service.status = MyStatus.FAILED
|
||||
```
|
||||
"""
|
||||
|
||||
pass
|
||||
@@ -83,8 +83,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
|
||||
def __check_instance_classes(self) -> None:
|
||||
for attr_name, attr_value in get_class_and_instance_attributes(self).items():
|
||||
# every class defined by the user should inherit from DataService
|
||||
if not attr_name.startswith("_DataService__"):
|
||||
# every class defined by the user should inherit from DataService if it is
|
||||
# assigned to a public attribute
|
||||
if not attr_name.startswith("_"):
|
||||
warn_if_instance_class_does_not_inherit_from_DataService(attr_value)
|
||||
|
||||
def __set_attribute_based_on_type( # noqa:CFQ002
|
||||
@@ -198,22 +199,10 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
value, readonly status, and documentation if any in the resulting dictionary.
|
||||
Attributes and methods starting with an underscore are ignored.
|
||||
|
||||
For attributes, methods, and properties unique to the class (not inherited from
|
||||
the base class), the method uses the format "<prefix>.<key>" for keys in the
|
||||
dictionary. If no prefix is provided, the key format is simply "<key>".
|
||||
|
||||
For nested DataService instances, the method serializes recursively and appends
|
||||
the key of the nested instance to the prefix in the format "<prefix>.<key>".
|
||||
|
||||
For nested DataService instances, the method serializes recursively.
|
||||
For attributes of type list, each item in the list is serialized individually.
|
||||
If an item in the list is an instance of DataService, it is serialized
|
||||
recursively with its key in the format "<prefix>.<key>.<item_id>", where
|
||||
"item_id" is the id of the item itself.
|
||||
|
||||
Args:
|
||||
prefix (str, optional): The prefix for each key in the serialized
|
||||
dictionary. This is mainly used when this method is called recursively to
|
||||
maintain the structure of nested instances.
|
||||
recursively.
|
||||
|
||||
Returns:
|
||||
dict: The serialized instance.
|
||||
@@ -321,19 +310,13 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
"readonly": True,
|
||||
"value": running_task_info,
|
||||
}
|
||||
elif isinstance(getattr(self.__class__, key, None), property):
|
||||
prop: property = getattr(self.__class__, key)
|
||||
result[key] = {
|
||||
"type": type(value).__name__,
|
||||
"value": value
|
||||
if not isinstance(value, u.Quantity)
|
||||
else {"magnitude": value.m, "unit": str(value.u)},
|
||||
"readonly": prop.fset is None,
|
||||
"doc": get_attribute_doc(prop),
|
||||
}
|
||||
elif isinstance(value, Enum):
|
||||
if type(value).__base__.__name__ == "ColouredEnum":
|
||||
val_type = "ColouredEnum"
|
||||
else:
|
||||
val_type = "Enum"
|
||||
result[key] = {
|
||||
"type": "Enum",
|
||||
"type": val_type,
|
||||
"value": value.name,
|
||||
"enum": {
|
||||
name: member.value
|
||||
@@ -352,6 +335,11 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
"doc": get_attribute_doc(value),
|
||||
}
|
||||
|
||||
if isinstance(getattr(self.__class__, key, None), property):
|
||||
prop: property = getattr(self.__class__, key)
|
||||
result[key]["readonly"] = prop.fset is None
|
||||
result[key]["doc"] = get_attribute_doc(prop)
|
||||
|
||||
return result
|
||||
|
||||
def update_DataService_attribute(
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.398bc7f8.css",
|
||||
"main.js": "/static/js/main.c348625e.js",
|
||||
"main.js": "/static/js/main.553ca0c9.js",
|
||||
"index.html": "/index.html",
|
||||
"main.398bc7f8.css.map": "/static/css/main.398bc7f8.css.map",
|
||||
"main.c348625e.js.map": "/static/js/main.c348625e.js.map"
|
||||
"main.553ca0c9.js.map": "/static/js/main.553ca0c9.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.398bc7f8.css",
|
||||
"static/js/main.c348625e.js"
|
||||
"static/js/main.553ca0c9.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.c348625e.js"></script><link href="/static/css/main.398bc7f8.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.553ca0c9.js"></script><link href="/static/css/main.398bc7f8.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
File diff suppressed because one or more lines are too long
1
src/pydase/frontend/static/js/main.553ca0c9.js.map
Normal file
1
src/pydase/frontend/static/js/main.553ca0c9.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -28,9 +28,8 @@ class AdditionalServerProtocol(Protocol):
|
||||
|
||||
This protocol sets the standard for how additional servers should be implemented
|
||||
to ensure compatibility with the main Server class. The protocol requires that
|
||||
any server implementing it should have an __init__ method for initialization, a
|
||||
serve method for starting the server, and an install_signal_handlers method for
|
||||
setting up signal handlers.
|
||||
any server implementing it should have an __init__ method for initialization and a
|
||||
serve method for starting the server.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
@@ -62,12 +61,6 @@ class AdditionalServerProtocol(Protocol):
|
||||
"""
|
||||
...
|
||||
|
||||
def install_signal_handlers(self) -> None:
|
||||
"""Sets up signal handlers for the server. This method is used to define how the
|
||||
server should respond to various system signals, such as SIGINT and SIGTERM.
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class AdditionalServer(TypedDict):
|
||||
"""
|
||||
@@ -257,13 +250,6 @@ class Server:
|
||||
info=self._info,
|
||||
**server["kwargs"],
|
||||
)
|
||||
try:
|
||||
addin_server.install_signal_handlers = lambda: None # type: ignore
|
||||
except Exception:
|
||||
logger.debug(
|
||||
"Additional server does not have a method called "
|
||||
"'install_signal_handlers'."
|
||||
)
|
||||
|
||||
server_name = (
|
||||
addin_server.__module__ + "." + addin_server.__class__.__name__
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
from loguru import logger
|
||||
|
||||
from pydase.utils.helpers import get_component_class_names
|
||||
|
||||
|
||||
def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> None:
|
||||
base_class_name = __value.__class__.__base__.__name__
|
||||
@@ -13,7 +15,8 @@ def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) ->
|
||||
"asyncio.unix_events",
|
||||
"_abc",
|
||||
]
|
||||
and base_class_name not in ["DataService", "list", "Enum"]
|
||||
and base_class_name
|
||||
not in ["DataService", "list", "Enum"] + get_component_class_names()
|
||||
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
|
||||
):
|
||||
logger.warning(
|
||||
|
||||
51
tests/components/test_coloured_enum.py
Normal file
51
tests/components/test_coloured_enum.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from pytest import CaptureFixture, LogCaptureFixture
|
||||
|
||||
from pydase.components.coloured_enum import ColouredEnum
|
||||
from pydase.data_service.data_service import DataService
|
||||
|
||||
from .. import caplog # noqa
|
||||
|
||||
|
||||
def test_ColouredEnum(capsys: CaptureFixture) -> None:
|
||||
class MyStatus(ColouredEnum):
|
||||
RUNNING = "#00FF00"
|
||||
FAILING = "#FF0000"
|
||||
|
||||
class ServiceClass(DataService):
|
||||
_status = MyStatus.RUNNING
|
||||
|
||||
@property
|
||||
def status(self) -> MyStatus:
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, value: MyStatus) -> None:
|
||||
# do something ...
|
||||
self._status = value
|
||||
|
||||
service = ServiceClass()
|
||||
|
||||
service.status = MyStatus.FAILING
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_output = sorted(
|
||||
[
|
||||
"ServiceClass.status = MyStatus.FAILING",
|
||||
]
|
||||
)
|
||||
actual_output = sorted(captured.out.strip().split("\n")) # type: ignore
|
||||
assert actual_output == expected_output
|
||||
|
||||
|
||||
def test_warning(caplog: LogCaptureFixture) -> None: # noqa
|
||||
class MyStatus(ColouredEnum):
|
||||
RUNNING = "#00FF00"
|
||||
FAILING = "#FF0000"
|
||||
|
||||
class ServiceClass(DataService):
|
||||
status = MyStatus.RUNNING
|
||||
|
||||
assert (
|
||||
"Warning: Class MyStatus does not inherit from DataService." not in caplog.text
|
||||
)
|
||||
64
tests/data_service/test_data_service.py
Normal file
64
tests/data_service/test_data_service.py
Normal file
@@ -0,0 +1,64 @@
|
||||
from enum import Enum
|
||||
|
||||
import pydase
|
||||
|
||||
|
||||
def test_enum_serialize() -> None:
|
||||
class EnumClass(Enum):
|
||||
FOO = "foo"
|
||||
BAR = "bar"
|
||||
|
||||
class EnumAttribute(pydase.DataService):
|
||||
def __init__(self) -> None:
|
||||
self.some_enum = EnumClass.FOO
|
||||
super().__init__()
|
||||
|
||||
class EnumPropertyWithoutSetter(pydase.DataService):
|
||||
def __init__(self) -> None:
|
||||
self._some_enum = EnumClass.FOO
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def some_enum(self) -> EnumClass:
|
||||
return self._some_enum
|
||||
|
||||
class EnumPropertyWithSetter(pydase.DataService):
|
||||
def __init__(self) -> None:
|
||||
self._some_enum = EnumClass.FOO
|
||||
super().__init__()
|
||||
|
||||
@property
|
||||
def some_enum(self) -> EnumClass:
|
||||
return self._some_enum
|
||||
|
||||
@some_enum.setter
|
||||
def some_enum(self, value: EnumClass) -> None:
|
||||
self._some_enum = value
|
||||
|
||||
assert EnumAttribute().serialize() == {
|
||||
"some_enum": {
|
||||
"type": "Enum",
|
||||
"value": "FOO",
|
||||
"enum": {"FOO": "foo", "BAR": "bar"},
|
||||
"readonly": False,
|
||||
"doc": None,
|
||||
}
|
||||
}
|
||||
assert EnumPropertyWithoutSetter().serialize() == {
|
||||
"some_enum": {
|
||||
"type": "Enum",
|
||||
"value": "FOO",
|
||||
"enum": {"FOO": "foo", "BAR": "bar"},
|
||||
"readonly": True,
|
||||
"doc": None,
|
||||
}
|
||||
}
|
||||
assert EnumPropertyWithSetter().serialize() == {
|
||||
"some_enum": {
|
||||
"type": "Enum",
|
||||
"value": "FOO",
|
||||
"enum": {"FOO": "foo", "BAR": "bar"},
|
||||
"readonly": False,
|
||||
"doc": None,
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ from pytest import LogCaptureFixture
|
||||
|
||||
from pydase import DataService
|
||||
|
||||
from . import caplog # noqa
|
||||
from .. import caplog # noqa
|
||||
|
||||
|
||||
def test_setattr_warnings(caplog: LogCaptureFixture) -> None: # noqa
|
||||
@@ -32,3 +32,19 @@ def test_private_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
|
||||
" Warning: You should not set private but rather protected attributes! Use "
|
||||
"_something instead of __something." in caplog.text
|
||||
)
|
||||
|
||||
|
||||
def test_protected_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
|
||||
class SubClass:
|
||||
name = "Hello"
|
||||
|
||||
class ServiceClass(DataService):
|
||||
def __init__(self) -> None:
|
||||
self._subclass = SubClass
|
||||
super().__init__()
|
||||
|
||||
ServiceClass()
|
||||
|
||||
assert (
|
||||
"Warning: Class SubClass does not inherit from DataService." not in caplog.text
|
||||
)
|
||||
Reference in New Issue
Block a user