mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-21 08:40:03 +02:00
commit
f76703340c
@ -0,0 +1,39 @@
|
||||
::: pydase.data_service
|
||||
handler: python
|
||||
|
||||
::: pydase.server.server
|
||||
handler: python
|
||||
|
||||
::: pydase.server.web_server
|
||||
handler: python
|
||||
|
||||
::: pydase.client
|
||||
handler: python
|
||||
|
||||
::: pydase.components
|
||||
handler: python
|
||||
|
||||
::: pydase.utils.serialization.serializer
|
||||
handler: python
|
||||
|
||||
::: pydase.utils.serialization.deserializer
|
||||
handler: python
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_root_toc_entry: false
|
||||
show_symbol_type_heading: true
|
||||
show_symbol_type_toc: true
|
||||
|
||||
::: pydase.utils.serialization.types
|
||||
handler: python
|
||||
|
||||
::: pydase.utils.decorators
|
||||
handler: python
|
||||
options:
|
||||
filters: ["!render_in_frontend"]
|
||||
|
||||
::: pydase.units
|
||||
handler: python
|
||||
|
||||
::: pydase.config
|
||||
handler: python
|
@ -5,7 +5,7 @@
|
||||
end="<!--getting-started-end-->"
|
||||
%}
|
||||
|
||||
[RESTful API]: ./user-guide/interaction/main.md#restful-api
|
||||
[Python RPC Client]: ./user-guide/interaction/main.md#python-rpc-client
|
||||
[RESTful API]: ./user-guide/interaction/README.md#restful-api
|
||||
[Python RPC Client]: ./user-guide/interaction/README.md#python-rpc-client
|
||||
[Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents
|
||||
[Components]: ./user-guide/Components.md
|
||||
|
@ -10,7 +10,7 @@
|
||||
[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
|
||||
[Customizing Web Interface]: ./user-guide/interaction/README.md#customization-options
|
||||
[Task Management]: ./user-guide/Tasks.md
|
||||
[Units]: ./user-guide/Understanding-Units.md
|
||||
[Property Validation]: ./user-guide/Validating-Property-Setters.md
|
||||
|
@ -5,6 +5,7 @@ charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
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"
|
||||
ghp-import==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
griffe==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
idna==3.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||
jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
markdown==3.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
@ -14,10 +15,12 @@ mkdocs-autorefs==1.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocs-get-deps==0.2.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-material-extensions==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocs-material==9.5.30 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocs-material==9.5.31 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocs-swagger-ui-tag==0.6.10 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocs==1.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocstrings==0.22.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocstrings-python==1.10.8 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocstrings==0.25.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mkdocstrings[python]==0.25.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
packaging==24.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
paginate==0.5.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
@ -16,7 +16,7 @@ In `pydase`, components are fundamental building blocks that bridge the Python b
|
||||
## 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.
|
||||
1. [**Tasks**](./Tasks.md): 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
|
||||
@ -348,7 +348,7 @@ In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value`
|
||||
|
||||
- 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.
|
||||
The `NumberSlider` is capable of [displaying units](./Understanding-Units.md) 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:
|
||||
|
||||
|
29
mkdocs.yml
29
mkdocs.yml
@ -6,7 +6,7 @@ nav:
|
||||
- Getting Started: getting-started.md
|
||||
- User Guide:
|
||||
- Components Guide: user-guide/Components.md
|
||||
- Interacting with pydase Services: user-guide/interaction/main.md
|
||||
- Interacting with pydase Services: user-guide/interaction/README.md
|
||||
- Achieving Service Persistence: user-guide/Service_Persistence.md
|
||||
- Understanding Tasks: user-guide/Tasks.md
|
||||
- Understanding Units: user-guide/Understanding-Units.md
|
||||
@ -46,7 +46,32 @@ markdown_extensions:
|
||||
plugins:
|
||||
- include-markdown
|
||||
- search
|
||||
- mkdocstrings
|
||||
- mkdocstrings:
|
||||
handlers:
|
||||
python:
|
||||
paths: [src] # search packages in the src folder
|
||||
import:
|
||||
- https://docs.python.org/3/objects.inv
|
||||
- https://docs.pydantic.dev/latest/objects.inv
|
||||
- https://confz.readthedocs.io/en/latest/objects.inv
|
||||
options:
|
||||
show_source: true
|
||||
inherited_members: true
|
||||
merge_init_into_class: true
|
||||
show_signature_annotations: true
|
||||
signature_crossrefs: true
|
||||
separate_signature: true
|
||||
docstring_options:
|
||||
ignore_init_summary: true
|
||||
# docstring_section_style: list
|
||||
heading_level: 2
|
||||
parameter_headings: true
|
||||
show_root_heading: true
|
||||
show_root_full_path: true
|
||||
show_symbol_type_heading: true
|
||||
show_symbol_type_toc: true
|
||||
# summary: true
|
||||
unwrap_annotated: true
|
||||
- swagger-ui-tag
|
||||
|
||||
watch:
|
||||
|
44
poetry.lock
generated
44
poetry.lock
generated
@ -769,6 +769,20 @@ python-dateutil = ">=2.8.1"
|
||||
[package.extras]
|
||||
dev = ["flake8", "markdown", "twine", "wheel"]
|
||||
|
||||
[[package]]
|
||||
name = "griffe"
|
||||
version = "1.1.0"
|
||||
description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "griffe-1.1.0-py3-none-any.whl", hash = "sha256:38ccc5721571c95ae427123074cf0dc0d36bce7c9701ab2ada9fe0566ff50c10"},
|
||||
{file = "griffe-1.1.0.tar.gz", hash = "sha256:c6328cbdec0d449549c1cc332f59227cd5603f903479d73e4425d828b782ffc3"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = ">=0.4"
|
||||
|
||||
[[package]]
|
||||
name = "h11"
|
||||
version = "0.14.0"
|
||||
@ -1212,21 +1226,24 @@ beautifulsoup4 = ">=4.11.1"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings"
|
||||
version = "0.22.0"
|
||||
version = "0.25.2"
|
||||
description = "Automatic documentation from sources, for MkDocs."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba"},
|
||||
{file = "mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256"},
|
||||
{file = "mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc"},
|
||||
{file = "mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=7.0"
|
||||
Jinja2 = ">=2.11.1"
|
||||
Markdown = ">=3.3"
|
||||
MarkupSafe = ">=1.1"
|
||||
mkdocs = ">=1.2"
|
||||
mkdocs = ">=1.4"
|
||||
mkdocs-autorefs = ">=0.3.1"
|
||||
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
|
||||
platformdirs = ">=2.2.0"
|
||||
pymdown-extensions = ">=6.3"
|
||||
|
||||
[package.extras]
|
||||
@ -1234,6 +1251,21 @@ crystal = ["mkdocstrings-crystal (>=0.3.4)"]
|
||||
python = ["mkdocstrings-python (>=0.5.2)"]
|
||||
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocstrings-python"
|
||||
version = "1.10.8"
|
||||
description = "A Python handler for mkdocstrings."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "mkdocstrings_python-1.10.8-py3-none-any.whl", hash = "sha256:bb12e76c8b071686617f824029cb1dfe0e9afe89f27fb3ad9a27f95f054dcd89"},
|
||||
{file = "mkdocstrings_python-1.10.8.tar.gz", hash = "sha256:5856a59cbebbb8deb133224a540de1ff60bded25e54d8beacc375bb133d39016"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
griffe = ">=0.49"
|
||||
mkdocstrings = ">=0.25"
|
||||
|
||||
[[package]]
|
||||
name = "multidict"
|
||||
version = "6.0.5"
|
||||
@ -2464,4 +2496,4 @@ multidict = ">=4.0"
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "54e25a68577a912301aa9125e3f3de545e03a199a79b2153c106285b92febbba"
|
||||
content-hash = "7131eddc2065147a18c145bb6da09492f03eb7fe050e968109cecb6044d17ed6"
|
||||
|
@ -38,7 +38,7 @@ optional = true
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.5.30"
|
||||
mkdocs-include-markdown-plugin = "^3.9.1"
|
||||
mkdocstrings = "^0.22.0"
|
||||
mkdocstrings = {extras = ["python"], version = "^0.25.2"}
|
||||
pymdown-extensions = "^10.1"
|
||||
mkdocs-swagger-ui-tag = "^0.6.10"
|
||||
|
||||
|
@ -43,10 +43,10 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
|
||||
via a socket.io client in an asyncio environment.
|
||||
|
||||
Args:
|
||||
sio_client (socketio.AsyncClient):
|
||||
sio_client:
|
||||
The socket.io client instance used for asynchronous communication with the
|
||||
pydase service server.
|
||||
loop (asyncio.AbstractEventLoop):
|
||||
loop:
|
||||
The event loop in which the client operations are managed and executed.
|
||||
|
||||
This class is used to create a proxy object that behaves like a local representation
|
||||
@ -84,19 +84,16 @@ class Client:
|
||||
connection, disconnection, and updates, and ensures that the proxy object is
|
||||
up-to-date with the server state.
|
||||
|
||||
Attributes:
|
||||
proxy (ProxyClass):
|
||||
A proxy object representing the remote service, facilitating interaction as
|
||||
if it were local.
|
||||
|
||||
Args:
|
||||
url (str):
|
||||
url:
|
||||
The URL of the pydase Socket.IO server. This should always contain the
|
||||
protocol and the hostname.
|
||||
|
||||
Examples:
|
||||
- wss://my-service.example.com # for secure connections, use wss
|
||||
- ws://localhost:8001
|
||||
block_until_connected (bool):
|
||||
|
||||
- `wss://my-service.example.com` # for secure connections, use wss
|
||||
- `ws://localhost:8001`
|
||||
block_until_connected:
|
||||
If set to True, the constructor will block until the connection to the
|
||||
service has been established. This is useful for ensuring the client is
|
||||
ready to use immediately after instantiation. Default is True.
|
||||
@ -112,6 +109,8 @@ class Client:
|
||||
self._sio = socketio.AsyncClient()
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self.proxy = ProxyClass(sio_client=self._sio, loop=self._loop)
|
||||
"""A proxy object representing the remote service, facilitating interaction as
|
||||
if it were local."""
|
||||
self._thread = threading.Thread(
|
||||
target=asyncio_loop_thread, args=(self._loop,), daemon=True
|
||||
)
|
||||
|
@ -7,6 +7,7 @@ class ColouredEnum(Enum):
|
||||
|
||||
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
|
||||
@ -14,19 +15,20 @@ class ColouredEnum(Enum):
|
||||
- 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 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
|
||||
@ -57,8 +59,7 @@ class ColouredEnum(Enum):
|
||||
my_service.status = MyStatus.FAILED
|
||||
```
|
||||
|
||||
Note
|
||||
----
|
||||
Note:
|
||||
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.
|
||||
"""
|
||||
|
@ -19,22 +19,26 @@ class DeviceConnection(pydase.data_service.DataService):
|
||||
to the device. This method should update the `self._connected` attribute to reflect
|
||||
the connection status:
|
||||
|
||||
>>> class MyDeviceConnection(DeviceConnection):
|
||||
... def connect(self) -> None:
|
||||
... # Implementation to connect to the device
|
||||
... # Update self._connected to `True` if connection is successful,
|
||||
... # `False` otherwise
|
||||
... ...
|
||||
```python
|
||||
class MyDeviceConnection(DeviceConnection):
|
||||
def connect(self) -> None:
|
||||
# Implementation to connect to the device
|
||||
# Update self._connected to `True` if connection is successful,
|
||||
# `False` otherwise
|
||||
...
|
||||
```
|
||||
|
||||
Optionally, if additional logic is needed to determine the connection status,
|
||||
the `connected` property can also be overridden:
|
||||
|
||||
>>> class MyDeviceConnection(DeviceConnection):
|
||||
... @property
|
||||
... def connected(self) -> bool:
|
||||
... # Custom logic to determine connection status
|
||||
... return some_custom_condition
|
||||
...
|
||||
```python
|
||||
class MyDeviceConnection(DeviceConnection):
|
||||
@property
|
||||
def connected(self) -> bool:
|
||||
# Custom logic to determine connection status
|
||||
return some_custom_condition
|
||||
|
||||
```
|
||||
|
||||
Frontend Representation
|
||||
-----------------------
|
||||
|
@ -11,19 +11,17 @@ class NumberSlider(DataService):
|
||||
This class models a UI slider for a data service, allowing for adjustments of a
|
||||
parameter within a specified range and increments.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
value (float, optional):
|
||||
Args:
|
||||
value:
|
||||
The initial value of the slider. Defaults to 0.
|
||||
min (float, optional):
|
||||
min_:
|
||||
The minimum value of the slider. Defaults to 0.
|
||||
max (float, optional):
|
||||
max_:
|
||||
The maximum value of the slider. Defaults to 100.
|
||||
step_size (float, optional):
|
||||
step_size:
|
||||
The increment/decrement step size of the slider. Defaults to 1.0.
|
||||
|
||||
Example:
|
||||
--------
|
||||
```python
|
||||
class MySlider(pydase.components.NumberSlider):
|
||||
def __init__(
|
||||
|
@ -6,18 +6,30 @@ from confz import BaseConfig, EnvSource
|
||||
|
||||
class OperationMode(BaseConfig): # type: ignore[misc]
|
||||
environment: Literal["testing", "development", "production"] = "development"
|
||||
"""The service's operation mode."""
|
||||
|
||||
CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"])
|
||||
|
||||
|
||||
class ServiceConfig(BaseConfig): # type: ignore[misc]
|
||||
"""Service configuration.
|
||||
|
||||
Variables can be set through environment variables prefixed with `SERVICE_` or an
|
||||
`.env` file containing those variables.
|
||||
"""
|
||||
|
||||
config_dir: Path = Path("config")
|
||||
"""Configuration directory"""
|
||||
web_port: int = 8001
|
||||
"""Web server port"""
|
||||
|
||||
CONFIG_SOURCES = EnvSource(allow_all=True, prefix="SERVICE_", file=".env")
|
||||
|
||||
|
||||
class WebServerConfig(BaseConfig): # type: ignore[misc]
|
||||
"""The service's web server configuration."""
|
||||
|
||||
generate_web_settings: bool = False
|
||||
"""Should generate web_settings.json file"""
|
||||
|
||||
CONFIG_SOURCES = EnvSource(allow=["GENERATE_WEB_SETTINGS"])
|
||||
|
@ -124,8 +124,10 @@ class DataServiceObserver(PropertyObserver):
|
||||
object.
|
||||
|
||||
Args:
|
||||
callback (Callable[[str, Any, dict[str, Any]]): The callback function to be
|
||||
registered. The function should have the following signature:
|
||||
callback:
|
||||
The callback function to be registered. The function should have the
|
||||
following signature:
|
||||
|
||||
- full_access_path (str): The full dot-notation access path of the
|
||||
changed attribute. This path indicates the location of the changed
|
||||
attribute within the observable object's structure.
|
||||
|
@ -33,17 +33,19 @@ def load_state(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
the value should be loaded from the JSON file.
|
||||
|
||||
Example:
|
||||
>>> class Service(pydase.DataService):
|
||||
... _name = "Service"
|
||||
...
|
||||
... @property
|
||||
... def name(self) -> str:
|
||||
... return self._name
|
||||
...
|
||||
... @name.setter
|
||||
... @load_state
|
||||
... def name(self, value: str) -> None:
|
||||
... self._name = value
|
||||
```python
|
||||
class Service(pydase.DataService):
|
||||
_name = "Service"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
@load_state
|
||||
def name(self, value: str) -> None:
|
||||
self._name = value
|
||||
```
|
||||
"""
|
||||
|
||||
func._load_state = True # type: ignore[attr-defined]
|
||||
@ -85,13 +87,11 @@ class StateManager:
|
||||
StateManager provides a snapshot of the DataService's state that is sufficiently
|
||||
accurate for initial rendering and interaction.
|
||||
|
||||
Attributes:
|
||||
cache (dict[str, Any]):
|
||||
A dictionary cache of the DataService's state.
|
||||
filename (str):
|
||||
The file name used for storing the DataService's state.
|
||||
service (DataService):
|
||||
Args:
|
||||
service:
|
||||
The DataService instance whose state is being managed.
|
||||
filename:
|
||||
The file name used for storing the DataService's state.
|
||||
|
||||
Note:
|
||||
The StateManager's cache updates are triggered by notifications and do not
|
||||
@ -200,9 +200,11 @@ class StateManager:
|
||||
It also handles type-specific conversions for the new value before setting it.
|
||||
|
||||
Args:
|
||||
path: A dot-separated string indicating the hierarchical path to the
|
||||
path:
|
||||
A dot-separated string indicating the hierarchical path to the
|
||||
attribute.
|
||||
value: The new value to set for the attribute.
|
||||
serialized_value:
|
||||
The serialized representation of the new value to set for the attribute.
|
||||
"""
|
||||
|
||||
try:
|
||||
|
@ -17,10 +17,10 @@ def validate_set(
|
||||
getter and check against the desired value.
|
||||
|
||||
Args:
|
||||
timeout (float):
|
||||
timeout:
|
||||
The maximum time (in seconds) to wait for the value to be within the
|
||||
precision boundary.
|
||||
precision (float | None):
|
||||
precision:
|
||||
The acceptable deviation from the desired value. If None, the value must be
|
||||
exact.
|
||||
"""
|
||||
@ -44,13 +44,11 @@ def has_validate_set_decorator(prop: property) -> bool:
|
||||
Checks if a property setter has been decorated with the `validate_set` decorator.
|
||||
|
||||
Args:
|
||||
prop (property):
|
||||
prop:
|
||||
The property to check.
|
||||
|
||||
Returns:
|
||||
bool:
|
||||
True if the property setter has the `validate_set` decorator, False
|
||||
otherwise.
|
||||
True if the property setter has the `validate_set` decorator, False otherwise.
|
||||
"""
|
||||
|
||||
property_setter = prop.fset
|
||||
@ -68,11 +66,11 @@ def _validate_value_was_correctly_set(
|
||||
specified `precision` and time `timeout`.
|
||||
|
||||
Args:
|
||||
obj (Observable):
|
||||
obj:
|
||||
The instance of the class containing the property.
|
||||
name (str):
|
||||
name:
|
||||
The name of the property to validate.
|
||||
value (Any):
|
||||
value:
|
||||
The desired value to check against.
|
||||
|
||||
Raises:
|
||||
|
@ -64,18 +64,17 @@ class AdditionalServerProtocol(Protocol):
|
||||
|
||||
|
||||
class AdditionalServer(TypedDict):
|
||||
"""
|
||||
A TypedDict that represents the configuration for an additional server to be run
|
||||
"""A TypedDict that represents the configuration for an additional server to be run
|
||||
alongside the main server.
|
||||
|
||||
This class is used to specify the server type, the port on which the server should
|
||||
run, and any additional keyword arguments that should be passed to the server when
|
||||
it's instantiated.
|
||||
"""
|
||||
|
||||
server: type[AdditionalServerProtocol]
|
||||
"""Server adhering to the
|
||||
[`AdditionalServerProtocol`][pydase.server.server.AdditionalServerProtocol]."""
|
||||
port: int
|
||||
"""Port on which the server should run."""
|
||||
kwargs: dict[str, Any]
|
||||
"""Additional keyword arguments that will be passed to the server's constructor """
|
||||
|
||||
|
||||
class Server:
|
||||
@ -83,29 +82,20 @@ class Server:
|
||||
The `Server` class provides a flexible server implementation for the `DataService`.
|
||||
|
||||
Args:
|
||||
service: DataService
|
||||
service:
|
||||
The DataService instance that this server will manage.
|
||||
host: str
|
||||
The host address for the server. Default is '0.0.0.0', which means all
|
||||
host:
|
||||
The host address for the server. Defaults to `'0.0.0.0'`, which means all
|
||||
available network interfaces.
|
||||
web_port: int
|
||||
The port number for the web server. Default is
|
||||
`pydase.config.ServiceConfig().web_port`.
|
||||
enable_web: bool
|
||||
Whether to enable the web server. Default is True.
|
||||
filename: str | Path | None
|
||||
web_port:
|
||||
The port number for the web server. Defaults to
|
||||
[`ServiceConfig().web_port`][pydase.config.ServiceConfig.web_port].
|
||||
enable_web:
|
||||
Whether to enable the web server.
|
||||
filename:
|
||||
Filename of the file managing the service state persistence.
|
||||
Defaults to None.
|
||||
additional_servers : list[AdditionalServer]
|
||||
A list of additional servers to run alongside the main server. Each entry in
|
||||
the list should be a dictionary with the following structure:
|
||||
- server: A class that adheres to the AdditionalServerProtocol. This
|
||||
class should have an `__init__` method that accepts the DataService
|
||||
instance, port, host, and optional keyword arguments, and a `serve`
|
||||
method that is a coroutine responsible for starting the server.
|
||||
- port: The port on which the additional server will be running.
|
||||
- kwargs: A dictionary containing additional keyword arguments that will
|
||||
be passed to the server's `__init__` method.
|
||||
additional_servers:
|
||||
A list of additional servers to run alongside the main server.
|
||||
|
||||
Here's an example of how you might define an additional server:
|
||||
|
||||
@ -145,7 +135,7 @@ class Server:
|
||||
)
|
||||
server.run()
|
||||
```
|
||||
**kwargs: Any
|
||||
**kwargs:
|
||||
Additional keyword arguments.
|
||||
"""
|
||||
|
||||
@ -214,7 +204,7 @@ class Server:
|
||||
)
|
||||
|
||||
server_task = self._loop.create_task(addin_server.serve())
|
||||
server_task.add_done_callback(self.handle_server_shutdown)
|
||||
server_task.add_done_callback(self._handle_server_shutdown)
|
||||
self.servers[server_name] = server_task
|
||||
if self._enable_web:
|
||||
self._web_server = WebServer(
|
||||
@ -225,10 +215,10 @@ class Server:
|
||||
)
|
||||
server_task = self._loop.create_task(self._web_server.serve())
|
||||
|
||||
server_task.add_done_callback(self.handle_server_shutdown)
|
||||
server_task.add_done_callback(self._handle_server_shutdown)
|
||||
self.servers["web"] = server_task
|
||||
|
||||
def handle_server_shutdown(self, task: asyncio.Task[Any]) -> None:
|
||||
def _handle_server_shutdown(self, task: asyncio.Task[Any]) -> None:
|
||||
"""Handle server shutdown. If the service should exit, do nothing. Else, make
|
||||
the service exit."""
|
||||
|
||||
|
@ -54,12 +54,15 @@ class RunMethodDict(TypedDict):
|
||||
exposed DataService.
|
||||
|
||||
Attributes:
|
||||
name (str): The name of the method to be run.
|
||||
parent_path (str): The access path for the parent object of the method to be
|
||||
run. This is used to construct the full access path for the method. For
|
||||
example, for an method with access path 'attr1.list_attr[0].method_name',
|
||||
'attr1.list_attr[0]' would be the parent_path.
|
||||
kwargs (dict[str, Any]): The arguments passed to the method.
|
||||
name:
|
||||
The name of the method to be run.
|
||||
parent_path:
|
||||
The access path for the parent object of the method to be run. This is used
|
||||
to construct the full access path for the method. For example, for an method
|
||||
with access path 'attr1.list_attr[0].method_name', 'attr1.list_attr[0]'
|
||||
would be the parent_path.
|
||||
kwargs:
|
||||
The arguments passed to the method.
|
||||
"""
|
||||
|
||||
name: str
|
||||
@ -76,15 +79,15 @@ def setup_sio_server(
|
||||
Sets up and configures a Socket.IO asynchronous server.
|
||||
|
||||
Args:
|
||||
observer (DataServiceObserver):
|
||||
observer:
|
||||
The observer managing state updates and communication.
|
||||
enable_cors (bool):
|
||||
enable_cors:
|
||||
Flag indicating whether CORS should be enabled for the server.
|
||||
loop (asyncio.AbstractEventLoop):
|
||||
loop:
|
||||
The event loop in which the server will run.
|
||||
|
||||
Returns:
|
||||
socketio.AsyncServer: The configured Socket.IO asynchronous server.
|
||||
The configured Socket.IO asynchronous server.
|
||||
"""
|
||||
|
||||
state_manager = observer.state_manager
|
||||
|
@ -25,41 +25,50 @@ API_VERSION = "v1"
|
||||
|
||||
class WebServer:
|
||||
"""
|
||||
Represents a web server that adheres to the AdditionalServerProtocol, designed to
|
||||
work with a DataService instance. This server facilitates client-server
|
||||
communication and state management through web protocols and socket connections.
|
||||
Represents a web server that adheres to the
|
||||
[`AdditionalServerProtocol`][pydase.server.server.AdditionalServerProtocol],
|
||||
designed to work with a [`DataService`][pydase.DataService] instance. This server
|
||||
facilitates client-server communication and state management through web protocols
|
||||
and socket connections.
|
||||
|
||||
The WebServer class initializes and manages a web server environment using FastAPI
|
||||
and Socket.IO, allowing for HTTP and WebSocket communications. It incorporates CORS
|
||||
(Cross-Origin Resource Sharing) support, custom CSS, and serves a frontend static
|
||||
files directory. It also initializes web server settings based on configuration
|
||||
files or generates default settings if necessary.
|
||||
The WebServer class initializes and manages a web server environment aiohttp and
|
||||
Socket.IO, allowing for HTTP and Socket.IO communications. It incorporates CORS
|
||||
(Cross-Origin Resource Sharing) support, custom CSS, and serves a static files
|
||||
directory. It also initializes web server settings based on configuration files or
|
||||
generates default settings if necessary.
|
||||
|
||||
Configuration for the web server (like service configuration directory and whether
|
||||
to generate new web settings) is determined in the following order of precedence:
|
||||
|
||||
1. Values provided directly to the constructor.
|
||||
2. Environment variable settings (via configuration classes like
|
||||
`pydase.config.ServiceConfig` and `pydase.config.WebServerConfig`).
|
||||
[`ServiceConfig`][pydase.config.ServiceConfig] and
|
||||
[`WebServerConfig`][pydase.config.WebServerConfig]).
|
||||
3. Default values defined in the configuration classes.
|
||||
|
||||
Args:
|
||||
data_service_observer (DataServiceObserver): Observer for the DataService,
|
||||
handling state updates and communication to connected clients.
|
||||
host (str): Hostname or IP address where the server is accessible. Commonly
|
||||
'0.0.0.0' to bind to all network interfaces.
|
||||
port (int): Port number on which the server listens. Typically in the range
|
||||
1024-65535 (non-standard ports).
|
||||
css (str | Path | None, optional): Path to a custom CSS file for styling the
|
||||
frontend. If None, no custom styles are applied. Defaults to None.
|
||||
enable_cors (bool, optional): Flag to enable or disable CORS policy. When True,
|
||||
CORS is enabled, allowing cross-origin requests. Defaults to True.
|
||||
config_dir (Path | None, optional): Path to the configuration
|
||||
directory where the web settings will be stored. Defaults to
|
||||
`pydase.config.ServiceConfig().config_dir`.
|
||||
generate_new_web_settings (bool | None, optional): Flag to enable or disable
|
||||
generation of new web settings if the configuration file is missing. Defaults
|
||||
to `pydase.config.WebServerConfig().generate_new_web_settings`.
|
||||
**kwargs (Any): Additional unused keyword arguments.
|
||||
data_service_observer:
|
||||
Observer for the [`DataService`][pydase.DataService], handling state updates and communication to
|
||||
connected clients.
|
||||
host:
|
||||
Hostname or IP address where the server is accessible. Commonly '0.0.0.0'
|
||||
to bind to all network interfaces.
|
||||
port:
|
||||
Port number on which the server listens. Typically in the range 1024-65535
|
||||
(non-standard ports).
|
||||
css:
|
||||
Path to a custom CSS file for styling the frontend. If None, no custom
|
||||
styles are applied. Defaults to None.
|
||||
enable_cors:
|
||||
Flag to enable or disable CORS policy. When True, CORS is enabled, allowing
|
||||
cross-origin requests. Defaults to True.
|
||||
config_dir:
|
||||
Path to the configuration directory where the web settings will be stored.
|
||||
Defaults to [`ServiceConfig().config_dir`][pydase.config.ServiceConfig.config_dir].
|
||||
generate_web_settings:
|
||||
Flag to enable or disable generation of new web settings if the
|
||||
configuration file is missing. Defaults to
|
||||
[`WebServerConfig().generate_web_settings`][pydase.config.WebServerConfig.generate_web_settings].
|
||||
"""
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
|
@ -21,18 +21,20 @@ def convert_to_quantity(
|
||||
Convert a given value into a pint.Quantity object with the specified unit.
|
||||
|
||||
Args:
|
||||
value (QuantityDict | float | int | Quantity):
|
||||
value:
|
||||
The value to be converted into a Quantity object.
|
||||
|
||||
- If value is a float or int, it will be directly converted to the specified
|
||||
unit.
|
||||
- If value is a dict, it must have keys 'magnitude' and 'unit' to represent
|
||||
the value and unit.
|
||||
- If value is a Quantity object, it will remain unchanged.\n
|
||||
unit (str, optional): The target unit for conversion. If empty and value is not
|
||||
a Quantity object, it will assume a unitless quantity.
|
||||
unit:
|
||||
The target unit for conversion. If empty and value is not a Quantity object,
|
||||
it will assume a unitless quantity.
|
||||
|
||||
Returns:
|
||||
Quantity: The converted value as a pint.Quantity object with the specified unit.
|
||||
The converted value as a pint.Quantity object with the specified unit.
|
||||
|
||||
Examples:
|
||||
>>> convert_to_quantity(5, 'm')
|
||||
@ -42,9 +44,9 @@ def convert_to_quantity(
|
||||
>>> convert_to_quantity(10.0 * u.units.V)
|
||||
<Quantity(10.0, 'volt')>
|
||||
|
||||
Notes:
|
||||
- If unit is not provided and value is a float or int, the resulting Quantity
|
||||
will be unitless.
|
||||
Note:
|
||||
If unit is not provided and value is a float or int, the resulting Quantity will
|
||||
be unitless.
|
||||
"""
|
||||
|
||||
if isinstance(value, int | float):
|
||||
|
@ -10,9 +10,9 @@ class FunctionDefinitionError(Exception):
|
||||
|
||||
|
||||
def frontend(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
"""
|
||||
Decorator to mark a DataService method for frontend rendering. Ensures that the
|
||||
method does not contain arguments, as they are not supported for frontend rendering.
|
||||
"""Decorator to mark a [`DataService`][pydase.DataService] method for frontend
|
||||
rendering. Ensures that the method does not contain arguments, as they are not
|
||||
supported for frontend rendering.
|
||||
"""
|
||||
|
||||
if function_has_arguments(func):
|
||||
|
@ -19,10 +19,15 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
import json
|
||||
|
||||
json.loads
|
||||
|
||||
|
||||
class Deserializer:
|
||||
@classmethod
|
||||
def deserialize(cls, serialized_object: SerializedObject) -> Any:
|
||||
"""Deserialize `serialized_object` (a `dict`) to a Python object."""
|
||||
type_handler: dict[str | None, None | Callable[..., Any]] = {
|
||||
None: None,
|
||||
"int": cls.deserialize_primitive,
|
||||
@ -159,4 +164,5 @@ class Deserializer:
|
||||
|
||||
|
||||
def loads(serialized_object: SerializedObject) -> Any:
|
||||
"""Deserialize `serialized_object` (a `dict`) to a Python object."""
|
||||
return Deserializer.deserialize(serialized_object)
|
||||
|
@ -52,8 +52,27 @@ class SerializationPathError(Exception):
|
||||
|
||||
|
||||
class Serializer:
|
||||
"""Serializes objects into
|
||||
[`SerializedObject`][pydase.utils.serialization.types.SerializedObject]
|
||||
representations.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def serialize_object(cls, obj: Any, access_path: str = "") -> SerializedObject: # noqa: C901
|
||||
"""Serialize `obj` to a
|
||||
[`SerializedObject`][pydase.utils.serialization.types.SerializedObject].
|
||||
|
||||
Args:
|
||||
obj:
|
||||
Object to be serialized.
|
||||
access_path:
|
||||
String corresponding to the full access path of the object. This will be
|
||||
prepended to the full_access_path in the SerializedObject entries.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of `obj`.
|
||||
"""
|
||||
|
||||
result: SerializedObject
|
||||
|
||||
if isinstance(obj, Exception):
|
||||
@ -313,6 +332,19 @@ class Serializer:
|
||||
|
||||
|
||||
def dump(obj: Any) -> SerializedObject:
|
||||
"""Serialize `obj` to a
|
||||
[`SerializedObject`][pydase.utils.serialization.types.SerializedObject].
|
||||
|
||||
The [`Serializer`][pydase.utils.serialization.serializer.Serializer] is used for
|
||||
encoding.
|
||||
|
||||
Args:
|
||||
obj:
|
||||
Object to be serialized.
|
||||
|
||||
Returns:
|
||||
Dictionary representation of `obj`.
|
||||
"""
|
||||
return Serializer.serialize_object(obj)
|
||||
|
||||
|
||||
@ -321,12 +353,13 @@ def set_nested_value_by_path(
|
||||
) -> None:
|
||||
"""
|
||||
Set a value in a nested dictionary structure, which conforms to the serialization
|
||||
format used by `pydase.utils.serializer.Serializer`, using a dot-notation path.
|
||||
format used by [`Serializer`][pydase.utils.serialization.serializer.Serializer],
|
||||
using a dot-notation path.
|
||||
|
||||
Args:
|
||||
serialization_dict:
|
||||
The base dictionary representing data serialized with
|
||||
`pydase.utils.serializer.Serializer`.
|
||||
[`Serializer`][pydase.utils.serialization.serializer.Serializer].
|
||||
path:
|
||||
The dot-notation path (e.g., 'attr1.attr2[0].attr3') indicating where to
|
||||
set the value.
|
||||
@ -334,7 +367,7 @@ def set_nested_value_by_path(
|
||||
The new value to set at the specified path.
|
||||
|
||||
Note:
|
||||
- If the index equals the length of the list, the function will append the
|
||||
If the index equals the length of the list, the function will append the
|
||||
serialized representation of the 'value' to the list.
|
||||
"""
|
||||
|
||||
@ -438,26 +471,24 @@ def get_container_item_by_key(
|
||||
) -> SerializedObject:
|
||||
"""
|
||||
Retrieve an item from a container specified by the passed key. Add an item to the
|
||||
container if allow_append is set to True.
|
||||
container if `allow_append` is set to `True`.
|
||||
|
||||
If specified keys or indexes do not exist, the function can append new elements to
|
||||
dictionaries and to lists if `allow_append` is True and the missing element is
|
||||
exactly the next sequential index (for lists).
|
||||
|
||||
Args:
|
||||
container: dict[str, SerializedObject] | list[SerializedObject]
|
||||
container:
|
||||
The container representing serialized data.
|
||||
key: str
|
||||
key:
|
||||
The key name representing the attribute in the dictionary, which may include
|
||||
direct keys or indexes (e.g., 'attr_name', '["key"]' or '[0]').
|
||||
allow_append: bool
|
||||
allow_append:
|
||||
Flag to allow appending a new entry if the specified index is out of range
|
||||
by exactly one position.
|
||||
|
||||
Returns:
|
||||
SerializedObject
|
||||
The dictionary or list item corresponding to the specified attribute and
|
||||
index.
|
||||
The dictionary or list item corresponding to the specified attribute and index.
|
||||
|
||||
Raises:
|
||||
SerializationPathError:
|
||||
@ -485,12 +516,11 @@ def get_data_paths_from_serialized_object( # noqa: C901
|
||||
Recursively extracts full access paths from a serialized object.
|
||||
|
||||
Args:
|
||||
serialized_obj (SerializedObject):
|
||||
serialized_obj:
|
||||
The dictionary representing the serialization of an object. Produced by
|
||||
`pydase.utils.serializer.Serializer`.
|
||||
|
||||
Returns:
|
||||
list[str]:
|
||||
A list of strings, each representing a full access path in the serialized
|
||||
object.
|
||||
"""
|
||||
@ -532,11 +562,10 @@ def generate_serialized_data_paths(
|
||||
Recursively extracts full access paths from a serialized DataService class instance.
|
||||
|
||||
Args:
|
||||
data (dict[str, SerializedObject]):
|
||||
data:
|
||||
The value of the "value" key of a serialized DataService class instance.
|
||||
|
||||
Returns:
|
||||
list[str]:
|
||||
A list of strings, each representing a full access path in the serialized
|
||||
object.
|
||||
"""
|
||||
@ -556,3 +585,6 @@ def serialized_dict_is_nested_object(serialized_dict: SerializedObject) -> bool:
|
||||
# We are excluding Quantity here as the value corresponding to the "value" key is
|
||||
# a dictionary of the form {"magnitude": ..., "unit": ...}
|
||||
return serialized_dict["type"] != "Quantity" and (isinstance(value, dict | list))
|
||||
|
||||
|
||||
__all__ = ["Serializer", "dump"]
|
||||
|
@ -123,3 +123,21 @@ SerializedObject = (
|
||||
| SerializedQuantity
|
||||
| SerializedNoValue
|
||||
)
|
||||
"""
|
||||
This type can be any of the following:
|
||||
|
||||
- SerializedBool
|
||||
- SerializedFloat
|
||||
- SerializedInteger
|
||||
- SerializedString
|
||||
- SerializedDatetime
|
||||
- SerializedList
|
||||
- SerializedDict
|
||||
- SerializedNoneType
|
||||
- SerializedMethod
|
||||
- SerializedException
|
||||
- SerializedDataService
|
||||
- SerializedEnum
|
||||
- SerializedQuantity
|
||||
- SerializedNoValue
|
||||
"""
|
||||
|
Loading…
x
Reference in New Issue
Block a user