Merge pull request #156 from tiqi-group/docs

Updates Docs
This commit is contained in:
Mose Müller 2024-08-20 13:01:17 +02:00 committed by GitHub
commit f76703340c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 461 additions and 286 deletions

View File

@ -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

View File

@ -5,7 +5,7 @@
end="<!--getting-started-end-->" end="<!--getting-started-end-->"
%} %}
[RESTful API]: ./user-guide/interaction/main.md#restful-api [RESTful API]: ./user-guide/interaction/README.md#restful-api
[Python RPC Client]: ./user-guide/interaction/main.md#python-rpc-client [Python RPC Client]: ./user-guide/interaction/README.md#python-rpc-client
[Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents [Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents
[Components]: ./user-guide/Components.md [Components]: ./user-guide/Components.md

View File

@ -10,7 +10,7 @@
[Defining DataService]: ./getting-started.md#defining-a-dataservice [Defining DataService]: ./getting-started.md#defining-a-dataservice
[Web Interface Access]: ./getting-started.md#accessing-the-web-interface [Web Interface Access]: ./getting-started.md#accessing-the-web-interface
[Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client [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 [Task Management]: ./user-guide/Tasks.md
[Units]: ./user-guide/Understanding-Units.md [Units]: ./user-guide/Understanding-Units.md
[Property Validation]: ./user-guide/Validating-Property-Setters.md [Property Validation]: ./user-guide/Validating-Property-Setters.md

View File

@ -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" 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" 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" 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" 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" 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" 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-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-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-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-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" 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" 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" 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" pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0"

View File

@ -16,7 +16,7 @@ In `pydase`, components are fundamental building blocks that bridge the Python b
## Method Components ## Method Components
Within the `DataService` class of `pydase`, only methods devoid of arguments can be represented in the frontend, classified into two distinct categories 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. 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 ```python
@ -348,7 +348,7 @@ In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value`
- Incorporating units in `NumberSlider` - 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: Here's how to implement a `NumberSlider` with unit display:

View File

@ -6,7 +6,7 @@ nav:
- Getting Started: getting-started.md - Getting Started: getting-started.md
- User Guide: - User Guide:
- Components Guide: user-guide/Components.md - 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 - Achieving Service Persistence: user-guide/Service_Persistence.md
- Understanding Tasks: user-guide/Tasks.md - Understanding Tasks: user-guide/Tasks.md
- Understanding Units: user-guide/Understanding-Units.md - Understanding Units: user-guide/Understanding-Units.md
@ -44,10 +44,35 @@ markdown_extensions:
plugins: plugins:
- include-markdown - include-markdown
- search - search
- mkdocstrings - mkdocstrings:
- swagger-ui-tag 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: watch:
- src/pydase - src/pydase

44
poetry.lock generated
View File

@ -769,6 +769,20 @@ python-dateutil = ">=2.8.1"
[package.extras] [package.extras]
dev = ["flake8", "markdown", "twine", "wheel"] 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]] [[package]]
name = "h11" name = "h11"
version = "0.14.0" version = "0.14.0"
@ -1212,21 +1226,24 @@ beautifulsoup4 = ">=4.11.1"
[[package]] [[package]]
name = "mkdocstrings" name = "mkdocstrings"
version = "0.22.0" version = "0.25.2"
description = "Automatic documentation from sources, for MkDocs." description = "Automatic documentation from sources, for MkDocs."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.8"
files = [ files = [
{file = "mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba"}, {file = "mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc"},
{file = "mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256"}, {file = "mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc"},
] ]
[package.dependencies] [package.dependencies]
click = ">=7.0"
Jinja2 = ">=2.11.1" Jinja2 = ">=2.11.1"
Markdown = ">=3.3" Markdown = ">=3.3"
MarkupSafe = ">=1.1" MarkupSafe = ">=1.1"
mkdocs = ">=1.2" mkdocs = ">=1.4"
mkdocs-autorefs = ">=0.3.1" mkdocs-autorefs = ">=0.3.1"
mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""}
platformdirs = ">=2.2.0"
pymdown-extensions = ">=6.3" pymdown-extensions = ">=6.3"
[package.extras] [package.extras]
@ -1234,6 +1251,21 @@ crystal = ["mkdocstrings-crystal (>=0.3.4)"]
python = ["mkdocstrings-python (>=0.5.2)"] python = ["mkdocstrings-python (>=0.5.2)"]
python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] 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]] [[package]]
name = "multidict" name = "multidict"
version = "6.0.5" version = "6.0.5"
@ -2464,4 +2496,4 @@ multidict = ">=4.0"
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.10" python-versions = "^3.10"
content-hash = "54e25a68577a912301aa9125e3f3de545e03a199a79b2153c106285b92febbba" content-hash = "7131eddc2065147a18c145bb6da09492f03eb7fe050e968109cecb6044d17ed6"

View File

@ -38,7 +38,7 @@ optional = true
[tool.poetry.group.docs.dependencies] [tool.poetry.group.docs.dependencies]
mkdocs-material = "^9.5.30" mkdocs-material = "^9.5.30"
mkdocs-include-markdown-plugin = "^3.9.1" mkdocs-include-markdown-plugin = "^3.9.1"
mkdocstrings = "^0.22.0" mkdocstrings = {extras = ["python"], version = "^0.25.2"}
pymdown-extensions = "^10.1" pymdown-extensions = "^10.1"
mkdocs-swagger-ui-tag = "^0.6.10" mkdocs-swagger-ui-tag = "^0.6.10"

View File

@ -43,10 +43,10 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
via a socket.io client in an asyncio environment. via a socket.io client in an asyncio environment.
Args: Args:
sio_client (socketio.AsyncClient): sio_client:
The socket.io client instance used for asynchronous communication with the The socket.io client instance used for asynchronous communication with the
pydase service server. pydase service server.
loop (asyncio.AbstractEventLoop): loop:
The event loop in which the client operations are managed and executed. 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 This class is used to create a proxy object that behaves like a local representation
@ -54,20 +54,20 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
while actually communicating over network protocols. while actually communicating over network protocols.
It can also be used as an attribute of a pydase service itself, e.g. It can also be used as an attribute of a pydase service itself, e.g.
```python ```python
import pydase import pydase
class MyService(pydase.DataService): class MyService(pydase.DataService):
proxy = pydase.Client( proxy = pydase.Client(
hostname="...", port=8001, block_until_connected=False hostname="...", port=8001, block_until_connected=False
).proxy ).proxy
if __name__ == "__main__": if __name__ == "__main__":
service = MyService() service = MyService()
server = pydase.Server(service, web_port=8002).run() server = pydase.Server(service, web_port=8002).run()
``` ```
""" """
def __init__( def __init__(
@ -84,19 +84,16 @@ class Client:
connection, disconnection, and updates, and ensures that the proxy object is connection, disconnection, and updates, and ensures that the proxy object is
up-to-date with the server state. 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: Args:
url (str): url:
The URL of the pydase Socket.IO server. This should always contain the The URL of the pydase Socket.IO server. This should always contain the
protocol and the hostname. protocol and the hostname.
Examples: Examples:
- wss://my-service.example.com # for secure connections, use wss
- ws://localhost:8001 - `wss://my-service.example.com` # for secure connections, use wss
block_until_connected (bool): - `ws://localhost:8001`
block_until_connected:
If set to True, the constructor will block until the connection to the 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 service has been established. This is useful for ensuring the client is
ready to use immediately after instantiation. Default is True. ready to use immediately after instantiation. Default is True.
@ -112,6 +109,8 @@ class Client:
self._sio = socketio.AsyncClient() self._sio = socketio.AsyncClient()
self._loop = asyncio.new_event_loop() self._loop = asyncio.new_event_loop()
self.proxy = ProxyClass(sio_client=self._sio, loop=self._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( self._thread = threading.Thread(
target=asyncio_loop_thread, args=(self._loop,), daemon=True target=asyncio_loop_thread, args=(self._loop,), daemon=True
) )

View File

@ -7,58 +7,59 @@ class ColouredEnum(Enum):
This class extends the standard Enum but requires its values to be valid CSS This class extends the standard Enum but requires its values to be valid CSS
colour codes. Supported colour formats include: colour codes. Supported colour formats include:
- Hexadecimal colours
- Hexadecimal colours with transparency - Hexadecimal colours
- RGB colours - Hexadecimal colours with transparency
- RGBA colours - RGB colours
- HSL colours - RGBA colours
- HSLA colours - HSL colours
- Predefined/Cross-browser colour names - HSLA colours
- Predefined/Cross-browser colour names
Refer to the this website for more details on colour formats: Refer to the this website for more details on colour formats:
(https://www.w3schools.com/cssref/css_colours_legal.php) (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 The behavior of this component in the UI depends on how it's defined in the data
service: 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
- As property without a setter: Displays as a coloured box with the key of the users to select and change its value from the frontend.
`ColouredEnum` as text inside, serving as a visual indicator without user - As property without a setter: Displays as a coloured box with the key of the
interaction. `ColouredEnum` as text inside, serving as a visual indicator without user
interaction.
Example: Example:
-------- ```python
```python import pydase.components as pyc
import pydase.components as pyc import pydase
import pydase
class MyStatus(pyc.ColouredEnum): class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Orange PENDING = "#FFA500" # Orange
RUNNING = "#0000FF80" # Transparent Blue RUNNING = "#0000FF80" # Transparent Blue
PAUSED = "rgb(169, 169, 169)" # Dark Gray PAUSED = "rgb(169, 169, 169)" # Dark Gray
RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow
COMPLETED = "hsl(120, 100%, 50%)" # Green COMPLETED = "hsl(120, 100%, 50%)" # Green
FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red
CANCELLED = "SlateGray" # Slate Gray CANCELLED = "SlateGray" # Slate Gray
class StatusExample(pydase.DataService): class StatusExample(pydase.DataService):
_status = MyStatus.RUNNING _status = MyStatus.RUNNING
@property @property
def status(self) -> MyStatus: def status(self) -> MyStatus:
return self._status return self._status
@status.setter @status.setter
def status(self, value: MyStatus) -> None: def status(self, value: MyStatus) -> None:
# Custom logic here... # Custom logic here...
self._status = value self._status = value
# Example usage: # Example usage:
my_service = StatusExample() my_service = StatusExample()
my_service.status = MyStatus.FAILED my_service.status = MyStatus.FAILED
``` ```
Note Note:
---- Each enumeration name and value must be unique. This means that you should use
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.
different colour formats when you want to use a colour multiple times.
""" """

View File

@ -19,22 +19,26 @@ class DeviceConnection(pydase.data_service.DataService):
to the device. This method should update the `self._connected` attribute to reflect to the device. This method should update the `self._connected` attribute to reflect
the connection status: the connection status:
>>> class MyDeviceConnection(DeviceConnection): ```python
... def connect(self) -> None: class MyDeviceConnection(DeviceConnection):
... # Implementation to connect to the device def connect(self) -> None:
... # Update self._connected to `True` if connection is successful, # Implementation to connect to the device
... # `False` otherwise # Update self._connected to `True` if connection is successful,
... ... # `False` otherwise
...
```
Optionally, if additional logic is needed to determine the connection status, Optionally, if additional logic is needed to determine the connection status,
the `connected` property can also be overridden: the `connected` property can also be overridden:
>>> class MyDeviceConnection(DeviceConnection): ```python
... @property class MyDeviceConnection(DeviceConnection):
... def connected(self) -> bool: @property
... # Custom logic to determine connection status def connected(self) -> bool:
... return some_custom_condition # Custom logic to determine connection status
... return some_custom_condition
```
Frontend Representation Frontend Representation
----------------------- -----------------------

View File

@ -11,76 +11,74 @@ class NumberSlider(DataService):
This class models a UI slider for a data service, allowing for adjustments of a This class models a UI slider for a data service, allowing for adjustments of a
parameter within a specified range and increments. parameter within a specified range and increments.
Parameters: Args:
----------- value:
value (float, optional): The initial value of the slider. Defaults to 0.
The initial value of the slider. Defaults to 0. min_:
min (float, optional): The minimum value of the slider. Defaults to 0.
The minimum value of the slider. Defaults to 0. max_:
max (float, optional): The maximum value of the slider. Defaults to 100.
The maximum value of the slider. Defaults to 100. step_size:
step_size (float, optional): The increment/decrement step size of the slider. Defaults to 1.0.
The increment/decrement step size of the slider. Defaults to 1.0.
Example: Example:
-------- ```python
```python class MySlider(pydase.components.NumberSlider):
class MySlider(pydase.components.NumberSlider): def __init__(
def __init__( self,
self, value: float = 0.0,
value: float = 0.0, min_: float = 0.0,
min_: float = 0.0, max_: float = 100.0,
max_: float = 100.0, step_size: float = 1.0,
step_size: float = 1.0, ) -> None:
) -> None: super().__init__(value, min_, max_, step_size)
super().__init__(value, min_, max_, step_size)
@property @property
def min(self) -> float: def min(self) -> float:
return self._min return self._min
@min.setter @min.setter
def min(self, value: float) -> None: def min(self, value: float) -> None:
self._min = value self._min = value
@property @property
def max(self) -> float: def max(self) -> float:
return self._max return self._max
@max.setter @max.setter
def max(self, value: float) -> None: def max(self, value: float) -> None:
self._max = value self._max = value
@property @property
def step_size(self) -> float: def step_size(self) -> float:
return self._step_size return self._step_size
@step_size.setter @step_size.setter
def step_size(self, value: float) -> None: def step_size(self, value: float) -> None:
self._step_size = value self._step_size = value
@property @property
def value(self) -> float: def value(self) -> float:
return self._value return self._value
@value.setter @value.setter
def value(self, value: float) -> None: def value(self, value: float) -> None:
if value < self._min or value > self._max: if value < self._min or value > self._max:
raise ValueError( raise ValueError(
"Value is either below allowed min or above max value." "Value is either below allowed min or above max value."
) )
self._value = value self._value = value
class MyService(pydase.DataService): class MyService(pydase.DataService):
def __init__(self) -> None: def __init__(self) -> None:
self.voltage = MyService() self.voltage = MyService()
# Modifying or accessing the voltage value: # Modifying or accessing the voltage value:
my_service = MyService() my_service = MyService()
my_service.voltage.value = 5 my_service.voltage.value = 5
print(my_service.voltage.value) # Output: 5 print(my_service.voltage.value) # Output: 5
``` ```
""" """
def __init__( def __init__(

View File

@ -6,18 +6,30 @@ from confz import BaseConfig, EnvSource
class OperationMode(BaseConfig): # type: ignore[misc] class OperationMode(BaseConfig): # type: ignore[misc]
environment: Literal["testing", "development", "production"] = "development" environment: Literal["testing", "development", "production"] = "development"
"""The service's operation mode."""
CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"]) CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"])
class ServiceConfig(BaseConfig): # type: ignore[misc] 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") config_dir: Path = Path("config")
"""Configuration directory"""
web_port: int = 8001 web_port: int = 8001
"""Web server port"""
CONFIG_SOURCES = EnvSource(allow_all=True, prefix="SERVICE_", file=".env") CONFIG_SOURCES = EnvSource(allow_all=True, prefix="SERVICE_", file=".env")
class WebServerConfig(BaseConfig): # type: ignore[misc] class WebServerConfig(BaseConfig): # type: ignore[misc]
"""The service's web server configuration."""
generate_web_settings: bool = False generate_web_settings: bool = False
"""Should generate web_settings.json file"""
CONFIG_SOURCES = EnvSource(allow=["GENERATE_WEB_SETTINGS"]) CONFIG_SOURCES = EnvSource(allow=["GENERATE_WEB_SETTINGS"])

View File

@ -124,8 +124,10 @@ class DataServiceObserver(PropertyObserver):
object. object.
Args: Args:
callback (Callable[[str, Any, dict[str, Any]]): The callback function to be callback:
registered. The function should have the following signature: 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 - full_access_path (str): The full dot-notation access path of the
changed attribute. This path indicates the location of the changed changed attribute. This path indicates the location of the changed
attribute within the observable object's structure. attribute within the observable object's structure.

View File

@ -33,17 +33,19 @@ def load_state(func: Callable[..., Any]) -> Callable[..., Any]:
the value should be loaded from the JSON file. the value should be loaded from the JSON file.
Example: Example:
>>> class Service(pydase.DataService): ```python
... _name = "Service" class Service(pydase.DataService):
... _name = "Service"
... @property
... def name(self) -> str: @property
... return self._name def name(self) -> str:
... return self._name
... @name.setter
... @load_state @name.setter
... def name(self, value: str) -> None: @load_state
... self._name = value def name(self, value: str) -> None:
self._name = value
```
""" """
func._load_state = True # type: ignore[attr-defined] 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 StateManager provides a snapshot of the DataService's state that is sufficiently
accurate for initial rendering and interaction. accurate for initial rendering and interaction.
Attributes: Args:
cache (dict[str, Any]): service:
A dictionary cache of the DataService's state.
filename (str):
The file name used for storing the DataService's state.
service (DataService):
The DataService instance whose state is being managed. The DataService instance whose state is being managed.
filename:
The file name used for storing the DataService's state.
Note: Note:
The StateManager's cache updates are triggered by notifications and do not 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. It also handles type-specific conversions for the new value before setting it.
Args: Args:
path: A dot-separated string indicating the hierarchical path to the path:
A dot-separated string indicating the hierarchical path to the
attribute. 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: try:

View File

@ -17,10 +17,10 @@ def validate_set(
getter and check against the desired value. getter and check against the desired value.
Args: Args:
timeout (float): timeout:
The maximum time (in seconds) to wait for the value to be within the The maximum time (in seconds) to wait for the value to be within the
precision boundary. precision boundary.
precision (float | None): precision:
The acceptable deviation from the desired value. If None, the value must be The acceptable deviation from the desired value. If None, the value must be
exact. 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. Checks if a property setter has been decorated with the `validate_set` decorator.
Args: Args:
prop (property): prop:
The property to check. The property to check.
Returns: 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 property_setter = prop.fset
@ -68,11 +66,11 @@ def _validate_value_was_correctly_set(
specified `precision` and time `timeout`. specified `precision` and time `timeout`.
Args: Args:
obj (Observable): obj:
The instance of the class containing the property. The instance of the class containing the property.
name (str): name:
The name of the property to validate. The name of the property to validate.
value (Any): value:
The desired value to check against. The desired value to check against.
Raises: Raises:

View File

@ -35,18 +35,18 @@ class AdditionalServerProtocol(Protocol):
Args: Args:
data_service_observer: data_service_observer:
Observer for the DataService, handling state updates and communication to Observer for the DataService, handling state updates and communication to
connected clients through injected callbacks. Can be utilized to access the connected clients through injected callbacks. Can be utilized to access the
service and state manager, and to add custom state-update callbacks. service and state manager, and to add custom state-update callbacks.
host: host:
Hostname or IP address where the server is accessible. Commonly '0.0.0.0' to Hostname or IP address where the server is accessible. Commonly '0.0.0.0' to
bind to all network interfaces. bind to all network interfaces.
port: port:
Port number on which the server listens. Typically in the range 1024-65535 Port number on which the server listens. Typically in the range 1024-65535
(non-standard ports). (non-standard ports).
**kwargs: **kwargs:
Any additional parameters required for initializing the server. These Any additional parameters required for initializing the server. These
parameters are specific to the server's implementation. parameters are specific to the server's implementation.
""" """
def __init__( def __init__(
@ -64,18 +64,17 @@ class AdditionalServerProtocol(Protocol):
class AdditionalServer(TypedDict): 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. 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: type[AdditionalServerProtocol]
"""Server adhering to the
[`AdditionalServerProtocol`][pydase.server.server.AdditionalServerProtocol]."""
port: int port: int
"""Port on which the server should run."""
kwargs: dict[str, Any] kwargs: dict[str, Any]
"""Additional keyword arguments that will be passed to the server's constructor """
class Server: class Server:
@ -83,29 +82,20 @@ class Server:
The `Server` class provides a flexible server implementation for the `DataService`. The `Server` class provides a flexible server implementation for the `DataService`.
Args: Args:
service: DataService service:
The DataService instance that this server will manage. The DataService instance that this server will manage.
host: str host:
The host address for the server. Default is '0.0.0.0', which means all The host address for the server. Defaults to `'0.0.0.0'`, which means all
available network interfaces. available network interfaces.
web_port: int web_port:
The port number for the web server. Default is The port number for the web server. Defaults to
`pydase.config.ServiceConfig().web_port`. [`ServiceConfig().web_port`][pydase.config.ServiceConfig.web_port].
enable_web: bool enable_web:
Whether to enable the web server. Default is True. Whether to enable the web server.
filename: str | Path | None filename:
Filename of the file managing the service state persistence. Filename of the file managing the service state persistence.
Defaults to None. additional_servers:
additional_servers : list[AdditionalServer] A list of additional servers to run alongside the main server.
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.
Here's an example of how you might define an additional server: Here's an example of how you might define an additional server:
@ -145,8 +135,8 @@ class Server:
) )
server.run() server.run()
``` ```
**kwargs: Any **kwargs:
Additional keyword arguments. Additional keyword arguments.
""" """
def __init__( # noqa: PLR0913 def __init__( # noqa: PLR0913
@ -214,7 +204,7 @@ class Server:
) )
server_task = self._loop.create_task(addin_server.serve()) 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 self.servers[server_name] = server_task
if self._enable_web: if self._enable_web:
self._web_server = WebServer( self._web_server = WebServer(
@ -225,10 +215,10 @@ class Server:
) )
server_task = self._loop.create_task(self._web_server.serve()) 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 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 """Handle server shutdown. If the service should exit, do nothing. Else, make
the service exit.""" the service exit."""

View File

@ -54,12 +54,15 @@ class RunMethodDict(TypedDict):
exposed DataService. exposed DataService.
Attributes: Attributes:
name (str): The name of the method to be run. name:
parent_path (str): The access path for the parent object of the method to be The name of the method to be run.
run. This is used to construct the full access path for the method. For parent_path:
example, for an method with access path 'attr1.list_attr[0].method_name', The access path for the parent object of the method to be run. This is used
'attr1.list_attr[0]' would be the parent_path. to construct the full access path for the method. For example, for an method
kwargs (dict[str, Any]): The arguments passed to the 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 name: str
@ -76,15 +79,15 @@ def setup_sio_server(
Sets up and configures a Socket.IO asynchronous server. Sets up and configures a Socket.IO asynchronous server.
Args: Args:
observer (DataServiceObserver): observer:
The observer managing state updates and communication. The observer managing state updates and communication.
enable_cors (bool): enable_cors:
Flag indicating whether CORS should be enabled for the server. Flag indicating whether CORS should be enabled for the server.
loop (asyncio.AbstractEventLoop): loop:
The event loop in which the server will run. The event loop in which the server will run.
Returns: Returns:
socketio.AsyncServer: The configured Socket.IO asynchronous server. The configured Socket.IO asynchronous server.
""" """
state_manager = observer.state_manager state_manager = observer.state_manager

View File

@ -25,41 +25,50 @@ API_VERSION = "v1"
class WebServer: class WebServer:
""" """
Represents a web server that adheres to the AdditionalServerProtocol, designed to Represents a web server that adheres to the
work with a DataService instance. This server facilitates client-server [`AdditionalServerProtocol`][pydase.server.server.AdditionalServerProtocol],
communication and state management through web protocols and socket connections. 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 The WebServer class initializes and manages a web server environment aiohttp and
and Socket.IO, allowing for HTTP and WebSocket communications. It incorporates CORS Socket.IO, allowing for HTTP and Socket.IO communications. It incorporates CORS
(Cross-Origin Resource Sharing) support, custom CSS, and serves a frontend static (Cross-Origin Resource Sharing) support, custom CSS, and serves a static files
files directory. It also initializes web server settings based on configuration directory. It also initializes web server settings based on configuration files or
files or generates default settings if necessary. generates default settings if necessary.
Configuration for the web server (like service configuration directory and whether Configuration for the web server (like service configuration directory and whether
to generate new web settings) is determined in the following order of precedence: to generate new web settings) is determined in the following order of precedence:
1. Values provided directly to the constructor. 1. Values provided directly to the constructor.
2. Environment variable settings (via configuration classes like 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. 3. Default values defined in the configuration classes.
Args: Args:
data_service_observer (DataServiceObserver): Observer for the DataService, data_service_observer:
handling state updates and communication to connected clients. Observer for the [`DataService`][pydase.DataService], handling state updates and communication to
host (str): Hostname or IP address where the server is accessible. Commonly connected clients.
'0.0.0.0' to bind to all network interfaces. host:
port (int): Port number on which the server listens. Typically in the range Hostname or IP address where the server is accessible. Commonly '0.0.0.0'
1024-65535 (non-standard ports). to bind to all network interfaces.
css (str | Path | None, optional): Path to a custom CSS file for styling the port:
frontend. If None, no custom styles are applied. Defaults to None. Port number on which the server listens. Typically in the range 1024-65535
enable_cors (bool, optional): Flag to enable or disable CORS policy. When True, (non-standard ports).
CORS is enabled, allowing cross-origin requests. Defaults to True. css:
config_dir (Path | None, optional): Path to the configuration Path to a custom CSS file for styling the frontend. If None, no custom
directory where the web settings will be stored. Defaults to styles are applied. Defaults to None.
`pydase.config.ServiceConfig().config_dir`. enable_cors:
generate_new_web_settings (bool | None, optional): Flag to enable or disable Flag to enable or disable CORS policy. When True, CORS is enabled, allowing
generation of new web settings if the configuration file is missing. Defaults cross-origin requests. Defaults to True.
to `pydase.config.WebServerConfig().generate_new_web_settings`. config_dir:
**kwargs (Any): Additional unused keyword arguments. 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 def __init__( # noqa: PLR0913

View File

@ -21,18 +21,20 @@ def convert_to_quantity(
Convert a given value into a pint.Quantity object with the specified unit. Convert a given value into a pint.Quantity object with the specified unit.
Args: Args:
value (QuantityDict | float | int | Quantity): value:
The value to be converted into a Quantity object. The value to be converted into a Quantity object.
- If value is a float or int, it will be directly converted to the specified - If value is a float or int, it will be directly converted to the specified
unit. unit.
- If value is a dict, it must have keys 'magnitude' and 'unit' to represent - If value is a dict, it must have keys 'magnitude' and 'unit' to represent
the value and unit. the value and unit.
- If value is a Quantity object, it will remain unchanged.\n - 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 unit:
a Quantity object, it will assume a unitless quantity. The target unit for conversion. If empty and value is not a Quantity object,
it will assume a unitless quantity.
Returns: 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: Examples:
>>> convert_to_quantity(5, 'm') >>> convert_to_quantity(5, 'm')
@ -42,9 +44,9 @@ def convert_to_quantity(
>>> convert_to_quantity(10.0 * u.units.V) >>> convert_to_quantity(10.0 * u.units.V)
<Quantity(10.0, 'volt')> <Quantity(10.0, 'volt')>
Notes: Note:
- If unit is not provided and value is a float or int, the resulting Quantity If unit is not provided and value is a float or int, the resulting Quantity will
will be unitless. be unitless.
""" """
if isinstance(value, int | float): if isinstance(value, int | float):

View File

@ -10,9 +10,9 @@ class FunctionDefinitionError(Exception):
def frontend(func: Callable[..., Any]) -> Callable[..., Any]: def frontend(func: Callable[..., Any]) -> Callable[..., Any]:
""" """Decorator to mark a [`DataService`][pydase.DataService] method for frontend
Decorator to mark a DataService method for frontend rendering. Ensures that the rendering. Ensures that the method does not contain arguments, as they are not
method does not contain arguments, as they are not supported for frontend rendering. supported for frontend rendering.
""" """
if function_has_arguments(func): if function_has_arguments(func):

View File

@ -19,10 +19,15 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import json
json.loads
class Deserializer: class Deserializer:
@classmethod @classmethod
def deserialize(cls, serialized_object: SerializedObject) -> Any: def deserialize(cls, serialized_object: SerializedObject) -> Any:
"""Deserialize `serialized_object` (a `dict`) to a Python object."""
type_handler: dict[str | None, None | Callable[..., Any]] = { type_handler: dict[str | None, None | Callable[..., Any]] = {
None: None, None: None,
"int": cls.deserialize_primitive, "int": cls.deserialize_primitive,
@ -159,4 +164,5 @@ class Deserializer:
def loads(serialized_object: SerializedObject) -> Any: def loads(serialized_object: SerializedObject) -> Any:
"""Deserialize `serialized_object` (a `dict`) to a Python object."""
return Deserializer.deserialize(serialized_object) return Deserializer.deserialize(serialized_object)

View File

@ -52,8 +52,27 @@ class SerializationPathError(Exception):
class Serializer: class Serializer:
"""Serializes objects into
[`SerializedObject`][pydase.utils.serialization.types.SerializedObject]
representations.
"""
@classmethod @classmethod
def serialize_object(cls, obj: Any, access_path: str = "") -> SerializedObject: # noqa: C901 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 result: SerializedObject
if isinstance(obj, Exception): if isinstance(obj, Exception):
@ -313,6 +332,19 @@ class Serializer:
def dump(obj: Any) -> SerializedObject: 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) return Serializer.serialize_object(obj)
@ -321,12 +353,13 @@ def set_nested_value_by_path(
) -> None: ) -> None:
""" """
Set a value in a nested dictionary structure, which conforms to the serialization 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: Args:
serialization_dict: serialization_dict:
The base dictionary representing data serialized with The base dictionary representing data serialized with
`pydase.utils.serializer.Serializer`. [`Serializer`][pydase.utils.serialization.serializer.Serializer].
path: path:
The dot-notation path (e.g., 'attr1.attr2[0].attr3') indicating where to The dot-notation path (e.g., 'attr1.attr2[0].attr3') indicating where to
set the value. set the value.
@ -334,8 +367,8 @@ def set_nested_value_by_path(
The new value to set at the specified path. The new value to set at the specified path.
Note: 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. serialized representation of the 'value' to the list.
""" """
path_parts = parse_full_access_path(path) path_parts = parse_full_access_path(path)
@ -438,26 +471,24 @@ def get_container_item_by_key(
) -> SerializedObject: ) -> SerializedObject:
""" """
Retrieve an item from a container specified by the passed key. Add an item to the 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 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 dictionaries and to lists if `allow_append` is True and the missing element is
exactly the next sequential index (for lists). exactly the next sequential index (for lists).
Args: Args:
container: dict[str, SerializedObject] | list[SerializedObject] container:
The container representing serialized data. The container representing serialized data.
key: str key:
The key name representing the attribute in the dictionary, which may include The key name representing the attribute in the dictionary, which may include
direct keys or indexes (e.g., 'attr_name', '["key"]' or '[0]'). 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 Flag to allow appending a new entry if the specified index is out of range
by exactly one position. by exactly one position.
Returns: 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: Raises:
SerializationPathError: SerializationPathError:
@ -485,13 +516,12 @@ def get_data_paths_from_serialized_object( # noqa: C901
Recursively extracts full access paths from a serialized object. Recursively extracts full access paths from a serialized object.
Args: Args:
serialized_obj (SerializedObject): serialized_obj:
The dictionary representing the serialization of an object. Produced by The dictionary representing the serialization of an object. Produced by
`pydase.utils.serializer.Serializer`. `pydase.utils.serializer.Serializer`.
Returns: Returns:
list[str]: A list of strings, each representing a full access path in the serialized
A list of strings, each representing a full access path in the serialized
object. object.
""" """
@ -532,12 +562,11 @@ def generate_serialized_data_paths(
Recursively extracts full access paths from a serialized DataService class instance. Recursively extracts full access paths from a serialized DataService class instance.
Args: Args:
data (dict[str, SerializedObject]): data:
The value of the "value" key of a serialized DataService class instance. The value of the "value" key of a serialized DataService class instance.
Returns: Returns:
list[str]: A list of strings, each representing a full access path in the serialized
A list of strings, each representing a full access path in the serialized
object. 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 # We are excluding Quantity here as the value corresponding to the "value" key is
# a dictionary of the form {"magnitude": ..., "unit": ...} # a dictionary of the form {"magnitude": ..., "unit": ...}
return serialized_dict["type"] != "Quantity" and (isinstance(value, dict | list)) return serialized_dict["type"] != "Quantity" and (isinstance(value, dict | list))
__all__ = ["Serializer", "dump"]

View File

@ -123,3 +123,21 @@ SerializedObject = (
| SerializedQuantity | SerializedQuantity
| SerializedNoValue | SerializedNoValue
) )
"""
This type can be any of the following:
- SerializedBool
- SerializedFloat
- SerializedInteger
- SerializedString
- SerializedDatetime
- SerializedList
- SerializedDict
- SerializedNoneType
- SerializedMethod
- SerializedException
- SerializedDataService
- SerializedEnum
- SerializedQuantity
- SerializedNoValue
"""