mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-22 01:00:02 +02:00
Merge pull request #147 from tiqi-group/feat/add_http_api_endpoints
Feat: add http API endpoints
This commit is contained in:
commit
c894215ddc
177
README.md
177
README.md
@ -1,9 +1,9 @@
|
|||||||
# pydase (Python Data Service) <!-- omit from toc -->
|
# pydase <!-- omit from toc -->
|
||||||
|
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://pydase.readthedocs.io/en/latest/?badge=latest)
|
[](https://pydase.readthedocs.io/en/latest/?badge=latest)
|
||||||
|
|
||||||
`pydase` is a Python library for creating data service servers with integrated web and RPC servers. It's designed to handle the management of data structures, automated tasks, and callbacks, and provides built-in functionality for serving data over different protocols.
|
`pydase` is a Python library designed to streamline the creation of services that interface with devices and data. It offers a unified API, simplifying the process of data querying and device interaction. Whether you're managing lab sensors, network devices, or any abstract data entity, `pydase` facilitates rapid service development and deployment.
|
||||||
|
|
||||||
- [Features](#features)
|
- [Features](#features)
|
||||||
- [Installation](#installation)
|
- [Installation](#installation)
|
||||||
@ -14,6 +14,7 @@
|
|||||||
- [Connecting to the Service via Python Client](#connecting-to-the-service-via-python-client)
|
- [Connecting to the Service via Python Client](#connecting-to-the-service-via-python-client)
|
||||||
- [Tab Completion Support](#tab-completion-support)
|
- [Tab Completion Support](#tab-completion-support)
|
||||||
- [Integration within Another Service](#integration-within-another-service)
|
- [Integration within Another Service](#integration-within-another-service)
|
||||||
|
- [RESTful API](#restful-api)
|
||||||
- [Understanding the Component System](#understanding-the-component-system)
|
- [Understanding the Component System](#understanding-the-component-system)
|
||||||
- [Built-in Type and Enum Components](#built-in-type-and-enum-components)
|
- [Built-in Type and Enum Components](#built-in-type-and-enum-components)
|
||||||
- [Method Components](#method-components)
|
- [Method Components](#method-components)
|
||||||
@ -45,11 +46,11 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
<!-- no toc -->
|
<!-- no toc -->
|
||||||
- [Simple data service definition through class-based interface](#defining-a-dataService)
|
- [Simple service definition through class-based interface](#defining-a-dataService)
|
||||||
- [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
|
- [Integrated web interface for interactive access and control of your service](#accessing-the-web-interface)
|
||||||
- [Support for programmatic control and interaction with your service](#connecting-to-the-service-via-python-client)
|
- [Support for programmatic control and interaction with your service](#connecting-to-the-service-via-python-client)
|
||||||
- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
|
- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
|
||||||
- [Customizable styling for the web interface through user-defined CSS](#customizing-web-interface-style)
|
- [Customizable styling for the web interface](#customizing-web-interface-style)
|
||||||
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
||||||
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
||||||
- [Support for units](#understanding-units-in-pydase)
|
- [Support for units](#understanding-units-in-pydase)
|
||||||
@ -209,6 +210,24 @@ In this setup, the `MyService` class has a `proxy` attribute that connects to a
|
|||||||
The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails.
|
The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails.
|
||||||
This configuration is particularly useful in distributed systems where services may start in any order.
|
This configuration is particularly useful in distributed systems where services may start in any order.
|
||||||
|
|
||||||
|
### RESTful API
|
||||||
|
The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes.
|
||||||
|
|
||||||
|
For example, you can get a value like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"http://<hostname>:<port>/api/v1/get_value?access_path=<full_access_path>"
|
||||||
|
)
|
||||||
|
serialized_value = json.loads(response.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, see [here](https://pydase.readthedocs.io/en/stable/user-guide/interaction/main/#restful-api).
|
||||||
|
|
||||||
<!--usage-end-->
|
<!--usage-end-->
|
||||||
|
|
||||||
## Understanding the Component System
|
## Understanding the Component System
|
||||||
@ -510,96 +529,96 @@ In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value`
|
|||||||
|
|
||||||
- Accessing parent class resources in `NumberSlider`
|
- Accessing parent class resources in `NumberSlider`
|
||||||
|
|
||||||
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
|
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
|
||||||
|
|
||||||
Here's an illustrative example:
|
Here's an illustrative example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
|
||||||
import pydase
|
import pydase
|
||||||
import pydase.components
|
import pydase.components
|
||||||
|
|
||||||
|
|
||||||
class MySlider(pydase.components.NumberSlider):
|
class MySlider(pydase.components.NumberSlider):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
value: float,
|
value: float,
|
||||||
on_change: Callable[[float], None],
|
on_change: Callable[[float], None],
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(value=value)
|
super().__init__(value=value)
|
||||||
self._on_change = on_change
|
self._on_change = on_change
|
||||||
|
|
||||||
# ... other properties ...
|
# ... other properties ...
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self) -> float:
|
def value(self) -> float:
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self, new_value: float) -> None:
|
def value(self, new_value: float) -> None:
|
||||||
if new_value < self._min or new_value > self._max:
|
if new_value < self._min or new_value > self._max:
|
||||||
raise ValueError("Value is either below allowed min or above max value.")
|
raise ValueError("Value is either below allowed min or above max value.")
|
||||||
self._value = new_value
|
self._value = new_value
|
||||||
self._on_change(new_value)
|
self._on_change(new_value)
|
||||||
|
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
class MyService(pydase.DataService):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.voltage = MySlider(
|
self.voltage = MySlider(
|
||||||
5,
|
5,
|
||||||
on_change=self.handle_voltage_change,
|
on_change=self.handle_voltage_change,
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle_voltage_change(self, new_voltage: float) -> None:
|
def handle_voltage_change(self, new_voltage: float) -> None:
|
||||||
print(f"Voltage changed to: {new_voltage}")
|
print(f"Voltage changed to: {new_voltage}")
|
||||||
# Additional logic here
|
# Additional logic here
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service_instance = MyService()
|
service_instance = MyService()
|
||||||
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
|
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
|
||||||
pydase.Server(service_instance).run()
|
pydase.Server(service_instance).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
- 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-in-pydase) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend.
|
||||||
|
|
||||||
Here's how to implement a `NumberSlider` with unit display:
|
Here's how to implement a `NumberSlider` with unit display:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import pydase
|
import pydase
|
||||||
import pydase.components
|
import pydase.components
|
||||||
import pydase.units as u
|
import pydase.units as u
|
||||||
|
|
||||||
class MySlider(pydase.components.NumberSlider):
|
class MySlider(pydase.components.NumberSlider):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
value: u.Quantity = 0.0 * u.units.V,
|
value: u.Quantity = 0.0 * u.units.V,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(value)
|
super().__init__(value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self) -> u.Quantity:
|
def value(self) -> u.Quantity:
|
||||||
return self._value
|
return self._value
|
||||||
|
|
||||||
@value.setter
|
@value.setter
|
||||||
def value(self, value: u.Quantity) -> None:
|
def value(self, value: u.Quantity) -> None:
|
||||||
if value.m < self._min or value.m > self._max:
|
if value.m < self._min or value.m > self._max:
|
||||||
raise ValueError("Value is either below allowed min or above max value.")
|
raise ValueError("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:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.voltage = MySlider()
|
self.voltage = MySlider()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service_instance = MyService()
|
service_instance = MyService()
|
||||||
service_instance.voltage.value = 5 * u.units.V
|
service_instance.voltage.value = 5 * u.units.V
|
||||||
print(service_instance.voltage.value) # Output: 5 V
|
print(service_instance.voltage.value) # Output: 5 V
|
||||||
pydase.Server(service_instance).run()
|
pydase.Server(service_instance).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `ColouredEnum`
|
#### `ColouredEnum`
|
||||||
|
|
||||||
@ -922,8 +941,8 @@ import pydase
|
|||||||
|
|
||||||
class Device(pydase.DataService):
|
class Device(pydase.DataService):
|
||||||
name = "My Device"
|
name = "My Device"
|
||||||
some_float = 1.0
|
temperature = 1.0
|
||||||
some_int = 1
|
power = 1
|
||||||
|
|
||||||
|
|
||||||
class Service(pydase.DataService):
|
class Service(pydase.DataService):
|
||||||
@ -946,11 +965,13 @@ with the following `web_settings.json`
|
|||||||
"device.name": {
|
"device.name": {
|
||||||
"display": false
|
"display": false
|
||||||
},
|
},
|
||||||
"device.some_float": {
|
"device.power": {
|
||||||
|
"displayName": "Power",
|
||||||
"displayOrder": 1
|
"displayOrder": 1
|
||||||
},
|
},
|
||||||
"device.some_int": {
|
"device.temperature": {
|
||||||
"displayOrder": 0
|
"displayName": "Temperature",
|
||||||
|
"displayOrder": 0
|
||||||
},
|
},
|
||||||
"state": {
|
"state": {
|
||||||
"displayOrder": 0
|
"displayOrder": 0
|
||||||
|
@ -111,7 +111,7 @@ Write the React component code, following the structure and patterns used in exi
|
|||||||
|
|
||||||
For example, for the `Image` component, a template could look like this:
|
For example, for the `Image` component, a template could look like this:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { Card, Collapse, Image } from 'react-bootstrap';
|
import { Card, Collapse, Image } from 'react-bootstrap';
|
||||||
import { DocStringComponent } from './DocStringComponent';
|
import { DocStringComponent } from './DocStringComponent';
|
||||||
@ -203,8 +203,7 @@ There are two different events a component might want to trigger: updating an at
|
|||||||
|
|
||||||
For illustration, take the `ButtonComponent`. When the button state changes, we want to send this update to the backend:
|
For illustration, take the `ButtonComponent`. When the button state changes, we want to send this update to the backend:
|
||||||
|
|
||||||
```tsx
|
```ts title="frontend/src/components/ButtonComponent.tsx"
|
||||||
// file: frontend/src/components/ButtonComponent.tsx
|
|
||||||
// ... (import statements)
|
// ... (import statements)
|
||||||
|
|
||||||
type ButtonComponentProps = {
|
type ButtonComponentProps = {
|
||||||
@ -249,7 +248,7 @@ There are two different events a component might want to trigger: updating an at
|
|||||||
|
|
||||||
To see how to use the `MethodComponent` in your component, have a look at the `DeviceConnection.tsx` file. Here is an example that demonstrates the usage of the `runMethod` function (also, have a look at the `MethodComponent.tsx` file):
|
To see how to use the `MethodComponent` in your component, have a look at the `DeviceConnection.tsx` file. Here is an example that demonstrates the usage of the `runMethod` function (also, have a look at the `MethodComponent.tsx` file):
|
||||||
|
|
||||||
```tsx
|
```ts title="frontend/src/components/_YourComponent_.tsx"
|
||||||
import { runMethod } from '../socket';
|
import { runMethod } from '../socket';
|
||||||
// ... (other imports)
|
// ... (other imports)
|
||||||
|
|
||||||
@ -287,9 +286,7 @@ The `GenericComponent` is responsible for rendering different types of component
|
|||||||
|
|
||||||
At the beginning of the `GenericComponent` file, import the newly created `ImageComponent`:
|
At the beginning of the `GenericComponent` file, import the newly created `ImageComponent`:
|
||||||
|
|
||||||
```tsx
|
```ts title="frontend/src/components/GenericComponent.tsx"
|
||||||
// file: frontend/src/components/GenericComponent.tsx
|
|
||||||
|
|
||||||
import { ImageComponent } from './ImageComponent';
|
import { ImageComponent } from './ImageComponent';
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -299,7 +296,7 @@ Update the `AttributeType` type definition to include the new type for the `Imag
|
|||||||
|
|
||||||
For example, if the new attribute type is `'Image'` (which should correspond to the name of the backend component class), you can add it to the union:
|
For example, if the new attribute type is `'Image'` (which should correspond to the name of the backend component class), you can add it to the union:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
type AttributeType =
|
type AttributeType =
|
||||||
| 'str'
|
| 'str'
|
||||||
| 'bool'
|
| 'bool'
|
||||||
@ -318,7 +315,7 @@ type AttributeType =
|
|||||||
|
|
||||||
Inside the `GenericComponent` function, add a new conditional branch to render the `ImageComponent` when the attribute type is `'Image'`:
|
Inside the `GenericComponent` function, add a new conditional branch to render the `ImageComponent` when the attribute type is `'Image'`:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
} else if (attribute.type === 'Image') {
|
} else if (attribute.type === 'Image') {
|
||||||
return (
|
return (
|
||||||
<ImageComponent
|
<ImageComponent
|
||||||
@ -348,7 +345,7 @@ For example, updating an `Image` component corresponds to setting a very long st
|
|||||||
|
|
||||||
To create a custom notification message, you can update the message passed to the `addNotification` method in the `useEffect` hook in the component file file. For the `ImageComponent`, this could look like this:
|
To create a custom notification message, you can update the message passed to the `addNotification` method in the `useEffect` hook in the component file file. For the `ImageComponent`, this could look like this:
|
||||||
|
|
||||||
```tsx
|
```ts
|
||||||
const fullAccessPath = [parentPath, name].filter((element) => element).join('.');
|
const fullAccessPath = [parentPath, name].filter((element) => element).join('.');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 22 KiB |
@ -1,20 +1,35 @@
|
|||||||
|
babel==2.15.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
beautifulsoup4==4.12.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
certifi==2024.7.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
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" and platform_system == "Windows"
|
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"
|
||||||
jinja2==3.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
idna==3.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
markdown==3.4.4 ; python_version >= "3.10" and python_version < "4.0"
|
jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
markupsafe==2.1.3 ; python_version >= "3.10" and python_version < "4.0"
|
markdown==3.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
markupsafe==2.1.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
mergedeep==1.3.4 ; python_version >= "3.10" and python_version < "4.0"
|
mergedeep==1.3.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
mkdocs-autorefs==0.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
mkdocs-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-include-markdown-plugin==3.9.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
mkdocs==1.5.3 ; python_version >= "3.10" and python_version < "4.0"
|
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-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==0.22.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
packaging==23.1 ; python_version >= "3.10" and python_version < "4.0"
|
packaging==24.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pathspec==0.11.2 ; python_version >= "3.10" and python_version < "4.0"
|
paginate==0.5.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
platformdirs==3.10.0 ; python_version >= "3.10" and python_version < "4.0"
|
pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pymdown-extensions==10.3 ; python_version >= "3.10" and python_version < "4.0"
|
platformdirs==4.2.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
python-dateutil==2.8.2 ; python_version >= "3.10" and python_version < "4.0"
|
pygments==2.18.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
pymdown-extensions==10.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pyyaml-env-tag==0.1 ; python_version >= "3.10" and python_version < "4.0"
|
pyyaml-env-tag==0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
regex==2024.7.24 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
requests==2.32.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
six==1.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
watchdog==3.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
soupsieve==2.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
urllib3==2.2.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
watchdog==4.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||||
|
167
docs/user-guide/interaction/Auto-generated Frontend.md
Normal file
167
docs/user-guide/interaction/Auto-generated Frontend.md
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
# Auto-generated Frontend
|
||||||
|
|
||||||
|
`pydase` automatically generates a frontend interface based on your service definition, representing the current state and controls of the service.
|
||||||
|
It simplifies the process of visualization and control of the data and devices managed by your `pydase` service, making it accessible to both developers and end-users.
|
||||||
|
|
||||||
|
Through the integration of Socket.IO, the frontend provides real-time updates, reflecting changes as they occur and allowing for immediate interaction with the backend.
|
||||||
|
|
||||||
|
|
||||||
|
## Accessing the Frontend
|
||||||
|
|
||||||
|
You can access the auto-generated frontend by navigating to the hostname of the device the service is hosted on, followed by the exposed port:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://<hostname>:<port>/
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend uses a component-based approach, representing various data types and control mechanisms as distinct UI components. For more information about this, please refer to [Components Guide](../Components.md).
|
||||||
|
|
||||||
|
## Customization Options
|
||||||
|
|
||||||
|
`pydase` allows you to enhance the user experience by customizing the web interface's appearance through
|
||||||
|
|
||||||
|
1. a custom CSS file, and
|
||||||
|
2. tailoring the frontend component layout and display style.
|
||||||
|
|
||||||
|
For more advanced customization, you can provide a completely custom frontend source.
|
||||||
|
|
||||||
|
### Custom CSS Styling
|
||||||
|
|
||||||
|
You can apply your own styles globally across the web interface by passing a custom CSS file to the server during initialization.
|
||||||
|
Here's how you can use this feature:
|
||||||
|
|
||||||
|
1. Prepare your custom CSS file with the desired styles.
|
||||||
|
|
||||||
|
2. When initializing your server, use the `css` parameter of the `Server` class to specify the path to your custom CSS file.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pydase import Server, DataService
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(DataService):
|
||||||
|
# ... your service definition ...
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service = MyService()
|
||||||
|
server = Server(service, css="path/to/your/custom.css").run()
|
||||||
|
```
|
||||||
|
|
||||||
|
This will apply the styles defined in `custom.css` to the web interface, allowing you to maintain branding consistency or improve visual accessibility.
|
||||||
|
|
||||||
|
Please ensure that the CSS file path is accessible from the server's running location. Relative or absolute paths can be used depending on your setup.
|
||||||
|
|
||||||
|
### Tailoring Frontend Component Layout
|
||||||
|
|
||||||
|
You can customize the display names, visibility, and order of components via the `web_settings.json` file.
|
||||||
|
Each key in the file corresponds to the full access path of public attributes, properties, and methods of the exposed service, using dot-notation.
|
||||||
|
|
||||||
|
- **Custom Display Names**: Modify the `"displayName"` value in the file to change how each component appears in the frontend.
|
||||||
|
- **Control Component Visibility**: Utilize the `"display"` key-value pair to control whether a component is rendered in the frontend. Set the value to `true` to make the component visible or `false` to hide it.
|
||||||
|
- **Adjustable Component Order**: The `"displayOrder"` values determine the order of components. Alter these values to rearrange the components as desired. The value defaults to [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
|
||||||
|
|
||||||
|
The `web_settings.json` file will be stored in the directory specified by `SERVICE_CONFIG_DIR`. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](#configuring-pydase-via-environment-variables).
|
||||||
|
|
||||||
|
For example, styling the following service
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase
|
||||||
|
|
||||||
|
|
||||||
|
class Device(pydase.DataService):
|
||||||
|
name = "My Device"
|
||||||
|
temperature = 1.0
|
||||||
|
power = 1
|
||||||
|
|
||||||
|
|
||||||
|
class Service(pydase.DataService):
|
||||||
|
device = Device()
|
||||||
|
state = "RUNNING"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pydase.Server(Service()).run()
|
||||||
|
```
|
||||||
|
|
||||||
|
with the following `web_settings.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"device": {
|
||||||
|
"displayName": "My Device",
|
||||||
|
"displayOrder": 1
|
||||||
|
},
|
||||||
|
"device.name": {
|
||||||
|
"display": false
|
||||||
|
},
|
||||||
|
"device.power": {
|
||||||
|
"displayName": "Power",
|
||||||
|
"displayOrder": 1
|
||||||
|
},
|
||||||
|
"device.temperature": {
|
||||||
|
"displayName": "Temperature",
|
||||||
|
"displayOrder": 0
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"displayOrder": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
looks like this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Specifying a Custom Frontend Source
|
||||||
|
|
||||||
|
To further customize your web interface, you can provide a custom frontend source.
|
||||||
|
By specifying the `frontend_src` parameter when initializing the server, you can host a tailored frontend application:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pydase
|
||||||
|
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
# Service definition
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service = MyService()
|
||||||
|
pydase.Server(
|
||||||
|
service,
|
||||||
|
frontend_src=Path("path/to/your/frontend/directory"),
|
||||||
|
).run()
|
||||||
|
```
|
||||||
|
|
||||||
|
`pydase` expects a directory structured as follows:
|
||||||
|
|
||||||
|
```bash title="Frontend directory structure"
|
||||||
|
<your_frontend_directory>
|
||||||
|
├── assets
|
||||||
|
│ └── ...
|
||||||
|
└── index.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Any CSS, js, image or other files need to be put into the assets folder for the web server to be able to provide access to it.
|
||||||
|
|
||||||
|
#### Example: Custom React Frontend
|
||||||
|
|
||||||
|
You can use vite to generate a react app template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm create vite@latest my-react-app -- --template react
|
||||||
|
```
|
||||||
|
|
||||||
|
*TODO: Add some useful information here...*
|
||||||
|
|
||||||
|
To deploy the custom react frontend, build it with
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
and pass the relative path of the output directory to the `frontend_src` parameter of the `pydase.Server`.
|
||||||
|
|
||||||
|
**Note** that you have to make sure that all the generated files (except the `index.html`) are in the `assets` folder. In the react app, you can achieve this by not using the `public` folder, but instead using e.g. `src/assets`.
|
45
docs/user-guide/interaction/Python Client.md
Normal file
45
docs/user-guide/interaction/Python Client.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Python Client
|
||||||
|
|
||||||
|
You can connect to the service using the `pydase.Client`. Below is an example of how to establish a connection to a service and interact with it:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase
|
||||||
|
|
||||||
|
# Replace the hostname and port with the IP address and the port of the machine
|
||||||
|
# where the service is running, respectively
|
||||||
|
client_proxy = pydase.Client(hostname="<ip_addr>", port=8001).proxy
|
||||||
|
|
||||||
|
# Interact with the service attributes as if they were local
|
||||||
|
client_proxy.voltage = 5.0
|
||||||
|
print(client_proxy.voltage) # Expected output: 5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
This example demonstrates setting and retrieving the `voltage` attribute through the client proxy.
|
||||||
|
The proxy acts as a local representative of the remote service, enabling straightforward interaction.
|
||||||
|
|
||||||
|
The proxy class dynamically synchronizes with the server's exposed attributes. This synchronization allows the proxy to be automatically updated with any attributes or methods that the server exposes, essentially mirroring the server's API. This dynamic updating enables users to interact with the remote service as if they were working with a local object.
|
||||||
|
|
||||||
|
## Tab Completion Support
|
||||||
|
|
||||||
|
In interactive environments such as Python interpreters and Jupyter notebooks, the proxy class supports tab completion, which allows users to explore available methods and attributes.
|
||||||
|
|
||||||
|
## Integration within Other Services
|
||||||
|
|
||||||
|
You can also integrate a client proxy within another service. Here's how you can set it up:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import pydase
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
# Initialize the client without blocking the constructor
|
||||||
|
proxy = pydase.Client(hostname="<ip_addr>", port=8001, block_until_connected=False).proxy
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
service = MyService()
|
||||||
|
# Create a server that exposes this service; adjust the web_port as needed
|
||||||
|
server = pydase.Server(service, web_port=8002). run()
|
||||||
|
```
|
||||||
|
|
||||||
|
In this setup, the `MyService` class has a `proxy` attribute that connects to a `pydase` service located at `<ip_addr>:8001`.
|
||||||
|
The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails.
|
||||||
|
This configuration is particularly useful in distributed systems where services may start in any order.
|
22
docs/user-guide/interaction/RESTful API.md
Normal file
22
docs/user-guide/interaction/RESTful API.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# RESTful API
|
||||||
|
|
||||||
|
The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. This is particularly useful for integrating `pydase` services with other applications or for scripting and automation.
|
||||||
|
|
||||||
|
For example, you can get a value like this:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
"http://<hostname>:<port>/api/v1/get_value?access_path=<full_access_path>"
|
||||||
|
)
|
||||||
|
serialized_value = json.loads(response.text)
|
||||||
|
```
|
||||||
|
|
||||||
|
To help developers understand and utilize the API, we provide an OpenAPI specification. This specification describes the available endpoints and corresponding request/response formats.
|
||||||
|
|
||||||
|
## OpenAPI Specification
|
||||||
|
|
||||||
|
<swagger-ui src="./openapi.yaml"/>
|
81
docs/user-guide/interaction/main.md
Normal file
81
docs/user-guide/interaction/main.md
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
# Interacting with `pydase` Services
|
||||||
|
|
||||||
|
`pydase` offers multiple ways for users to interact with the services they create, providing flexibility and convenience for different use cases. This section outlines the primary interaction methods available, including an auto-generated frontend, a RESTful API, and a Python client based on Socket.IO.
|
||||||
|
|
||||||
|
{%
|
||||||
|
include-markdown "./Auto-generated Frontend.md"
|
||||||
|
heading-offset=1
|
||||||
|
%}
|
||||||
|
|
||||||
|
{%
|
||||||
|
include-markdown "./RESTful API.md"
|
||||||
|
heading-offset=1
|
||||||
|
%}
|
||||||
|
|
||||||
|
{%
|
||||||
|
include-markdown "./Python Client.md"
|
||||||
|
heading-offset=1
|
||||||
|
%}
|
||||||
|
|
||||||
|
<!-- ## 2. **Socket.IO for Real-Time Updates** -->
|
||||||
|
<!-- For scenarios requiring real-time data updates, `pydase` includes a Socket.IO server. This feature is ideal for applications where live data tracking is crucial, such as monitoring systems or interactive dashboards. -->
|
||||||
|
<!---->
|
||||||
|
<!-- ### Key Features: -->
|
||||||
|
<!-- - **Live Data Streams**: Receive real-time updates for data changes. -->
|
||||||
|
<!-- - **Event-Driven Communication**: Utilize event-based messaging to push updates and handle client actions. -->
|
||||||
|
<!---->
|
||||||
|
<!-- ### Example Usage: -->
|
||||||
|
<!-- Clients can connect to the Socket.IO server to receive updates: -->
|
||||||
|
<!-- ```javascript -->
|
||||||
|
<!-- var socket = io.connect('http://<hostname>:<port>'); -->
|
||||||
|
<!-- socket.on('<event_name>', function(data) { -->
|
||||||
|
<!-- console.log(data); -->
|
||||||
|
<!-- }); -->
|
||||||
|
<!-- ``` -->
|
||||||
|
<!---->
|
||||||
|
<!-- **Use Cases:** -->
|
||||||
|
<!---->
|
||||||
|
<!-- - Real-time monitoring and alerts -->
|
||||||
|
<!-- - Live data visualization -->
|
||||||
|
<!-- - Collaborative applications -->
|
||||||
|
<!---->
|
||||||
|
<!-- ## 3. **Auto-Generated Frontend** -->
|
||||||
|
<!-- `pydase` automatically generates a web frontend based on the service definitions. This frontend is a convenient interface for interacting with the service, especially for users who prefer a graphical interface over command-line or code-based interactions. -->
|
||||||
|
<!---->
|
||||||
|
<!-- ### Key Features: -->
|
||||||
|
<!-- - **User-Friendly Interface**: Intuitive and easy to use, with real-time interaction capabilities. -->
|
||||||
|
<!-- - **Customizable**: Adjust the frontend's appearance and functionality to suit specific needs. -->
|
||||||
|
<!---->
|
||||||
|
<!-- ### Accessing the Frontend: -->
|
||||||
|
<!-- Once the service is running, access the frontend via a web browser: -->
|
||||||
|
<!-- ``` -->
|
||||||
|
<!-- http://<hostname>:<port> -->
|
||||||
|
<!-- ``` -->
|
||||||
|
<!---->
|
||||||
|
<!-- **Use Cases:** -->
|
||||||
|
<!---->
|
||||||
|
<!-- - End-user interfaces for data control and visualization -->
|
||||||
|
<!-- - Rapid prototyping and testing -->
|
||||||
|
<!-- - Demonstrations and training -->
|
||||||
|
<!---->
|
||||||
|
<!-- ## 4. **Python Client** -->
|
||||||
|
<!-- `pydase` also provides a Python client for programmatic interactions. This client is particularly useful for developers who want to integrate `pydase` services into other Python applications or automate interactions. -->
|
||||||
|
<!---->
|
||||||
|
<!-- ### Key Features: -->
|
||||||
|
<!-- - **Direct Interaction**: Call methods and access properties as if they were local. -->
|
||||||
|
<!-- - **Tab Completion**: Supports tab completion in interactive environments like Jupyter notebooks. -->
|
||||||
|
<!---->
|
||||||
|
<!-- ### Example Usage: -->
|
||||||
|
<!-- ```python -->
|
||||||
|
<!-- import pydase -->
|
||||||
|
<!---->
|
||||||
|
<!-- client = pydase.Client(hostname="<ip_addr>", port=8001) -->
|
||||||
|
<!-- service = client.proxy -->
|
||||||
|
<!-- service.some_method() -->
|
||||||
|
<!-- ``` -->
|
||||||
|
<!---->
|
||||||
|
<!-- **Use Cases:** -->
|
||||||
|
<!---->
|
||||||
|
<!-- - Integrating with other Python applications -->
|
||||||
|
<!-- - Automation and scripting -->
|
||||||
|
<!-- - Data analysis and manipulation -->
|
326
docs/user-guide/interaction/openapi.yaml
Normal file
326
docs/user-guide/interaction/openapi.yaml
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
version: 1.0.0
|
||||||
|
title: pydase API
|
||||||
|
tags:
|
||||||
|
- name: /api/v1
|
||||||
|
description: Version 1
|
||||||
|
paths:
|
||||||
|
/api/v1/get_value:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- /api/v1
|
||||||
|
summary: Get the value of an existing attribute.
|
||||||
|
description: Get the value of an existing attribute by full access path.
|
||||||
|
operationId: getValue
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: access_path
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: device.channel[0].voltage
|
||||||
|
required: true
|
||||||
|
description: Full access path of the service attribute.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful Operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SerializedAttribute'
|
||||||
|
examples:
|
||||||
|
Exists:
|
||||||
|
summary: Attribute exists
|
||||||
|
value:
|
||||||
|
docs: My documentation string.
|
||||||
|
full_access_path: device.channel[0].voltage
|
||||||
|
readonly: false
|
||||||
|
type: float
|
||||||
|
value: 12.1
|
||||||
|
'400':
|
||||||
|
description: Could not get attribute
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SerializedException'
|
||||||
|
examples:
|
||||||
|
Attribute:
|
||||||
|
summary: Attribute does not exist
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: AttributeError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "'MyService' object has no attribute 'invalid_attribute'"
|
||||||
|
List:
|
||||||
|
summary: List index out of range
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: IndexError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "list index out of range"
|
||||||
|
/api/v1/update_value:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- /api/v1
|
||||||
|
summary: Update an existing attribute.
|
||||||
|
description: Update an existing attribute by full access path.
|
||||||
|
operationId: updateValue
|
||||||
|
requestBody:
|
||||||
|
description: Update an existent attribute in the service
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/UpdateValue'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful Operation
|
||||||
|
'400':
|
||||||
|
description: Could not Update Attribute
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SerializedException'
|
||||||
|
examples:
|
||||||
|
Attribute:
|
||||||
|
summary: Attribute does not exist
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: AttributeError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "'MyService' object has no attribute 'invalid_attribute'"
|
||||||
|
ReadOnly:
|
||||||
|
summary: Attribute is read-only
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: AttributeError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "property 'readonly_property' of 'MyService' object has no setter"
|
||||||
|
List:
|
||||||
|
summary: List index out of range
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: IndexError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "list index out of range"
|
||||||
|
/api/v1/trigger_method:
|
||||||
|
put:
|
||||||
|
tags:
|
||||||
|
- /api/v1
|
||||||
|
summary: Trigger method.
|
||||||
|
description: Trigger method with by full access path with provided args and kwargs.
|
||||||
|
operationId: triggerMethod
|
||||||
|
requestBody:
|
||||||
|
description: Update an existent attribute in the service
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/TriggerMethod'
|
||||||
|
required: true
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Successful Operation
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SerializedAttribute'
|
||||||
|
examples:
|
||||||
|
NoneReturn:
|
||||||
|
summary: Function returns None
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
readonly: false
|
||||||
|
type: "NoneType"
|
||||||
|
value: null
|
||||||
|
FloatReturn:
|
||||||
|
summary: Function returns float
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
readonly: false
|
||||||
|
type: "float"
|
||||||
|
value: 23.2
|
||||||
|
'400':
|
||||||
|
description: Method does not exist
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/SerializedException'
|
||||||
|
examples:
|
||||||
|
Args:
|
||||||
|
summary: Wrong number of arguments
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: TypeError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "MyService.some_function() takes 1 positional argument but 2 were given"
|
||||||
|
Attribute:
|
||||||
|
summary: Attribute does not exist
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: AttributeError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "'MyService' object has no attribute 'invalid_method'"
|
||||||
|
List:
|
||||||
|
summary: List index out of range
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: IndexError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "list index out of range"
|
||||||
|
Dict:
|
||||||
|
summary: Dictionary key does not exist
|
||||||
|
value:
|
||||||
|
docs: null
|
||||||
|
full_access_path: ""
|
||||||
|
name: KeyError
|
||||||
|
readonly: true
|
||||||
|
type: Exception
|
||||||
|
value: "invalid_key"
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
UpdateValue:
|
||||||
|
required:
|
||||||
|
- access_path
|
||||||
|
- value
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
access_path:
|
||||||
|
type: string
|
||||||
|
example: device.channel[0].voltage
|
||||||
|
value:
|
||||||
|
$ref: '#/components/schemas/SerializedValue'
|
||||||
|
TriggerMethod:
|
||||||
|
required:
|
||||||
|
- access_path
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
access_path:
|
||||||
|
type: string
|
||||||
|
example: device.channel[0].voltage
|
||||||
|
args:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- value
|
||||||
|
- full_access_path
|
||||||
|
properties:
|
||||||
|
full_access_path:
|
||||||
|
type: string
|
||||||
|
example: ""
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- list
|
||||||
|
value:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SerializedValue'
|
||||||
|
kwargs:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- type
|
||||||
|
- value
|
||||||
|
- full_access_path
|
||||||
|
properties:
|
||||||
|
full_access_path:
|
||||||
|
type: string
|
||||||
|
example: ""
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- dict
|
||||||
|
value:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
$ref: '#/components/schemas/SerializedValue'
|
||||||
|
SerializedValue:
|
||||||
|
required:
|
||||||
|
- full_access_path
|
||||||
|
- type
|
||||||
|
- value
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
docs:
|
||||||
|
type: string | null
|
||||||
|
example: null
|
||||||
|
full_access_path:
|
||||||
|
type: string
|
||||||
|
example: ""
|
||||||
|
readonly:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
example: float
|
||||||
|
value:
|
||||||
|
type: any
|
||||||
|
example: 22.0
|
||||||
|
SerializedAttribute:
|
||||||
|
required:
|
||||||
|
- full_access_path
|
||||||
|
- type
|
||||||
|
- value
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
docs:
|
||||||
|
type: string | null
|
||||||
|
example: My documentation string.
|
||||||
|
full_access_path:
|
||||||
|
type: string
|
||||||
|
example: device.channel[0].voltage
|
||||||
|
readonly:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
example: float
|
||||||
|
value:
|
||||||
|
type: any
|
||||||
|
example: 22.0
|
||||||
|
SerializedException:
|
||||||
|
required:
|
||||||
|
- full_access_path
|
||||||
|
- type
|
||||||
|
- value
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
docs:
|
||||||
|
type: string | null
|
||||||
|
example: Raised when the access path does not correspond to a valid attribute.
|
||||||
|
full_access_path:
|
||||||
|
type: string
|
||||||
|
example: ""
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: SerializationPathError
|
||||||
|
readonly:
|
||||||
|
type: boolean
|
||||||
|
example: true
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
example: Exception
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
examples:
|
||||||
|
value:
|
||||||
|
"Index '2': list index out of range"
|
||||||
|
some:
|
||||||
|
"Index '2': list index out of range"
|
13
mkdocs.yml
13
mkdocs.yml
@ -6,6 +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
|
||||||
- Developer Guide:
|
- Developer Guide:
|
||||||
- Developer Guide: dev-guide/README.md
|
- Developer Guide: dev-guide/README.md
|
||||||
- API Reference: dev-guide/api.md
|
- API Reference: dev-guide/api.md
|
||||||
@ -16,7 +17,10 @@ nav:
|
|||||||
- Contributing: about/contributing.md
|
- Contributing: about/contributing.md
|
||||||
- License: about/license.md
|
- License: about/license.md
|
||||||
|
|
||||||
theme: readthedocs
|
theme:
|
||||||
|
name: material
|
||||||
|
features:
|
||||||
|
- content.code.copy
|
||||||
|
|
||||||
extra_css:
|
extra_css:
|
||||||
- css/extra.css
|
- css/extra.css
|
||||||
@ -26,17 +30,20 @@ markdown_extensions:
|
|||||||
- toc:
|
- toc:
|
||||||
permalink: true
|
permalink: true
|
||||||
- pymdownx.highlight:
|
- pymdownx.highlight:
|
||||||
|
use_pygments: true
|
||||||
anchor_linenums: true
|
anchor_linenums: true
|
||||||
|
line_spans: __span
|
||||||
|
pygments_lang_class: true
|
||||||
- pymdownx.snippets
|
- pymdownx.snippets
|
||||||
- pymdownx.superfences
|
- pymdownx.superfences
|
||||||
# - pymdownx.highlight:
|
- pymdownx.inlinehilite
|
||||||
# - pymdownx.inlinehilite
|
|
||||||
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- include-markdown
|
- include-markdown
|
||||||
- search
|
- search
|
||||||
- mkdocstrings
|
- mkdocstrings
|
||||||
|
- swagger-ui-tag
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
- src/pydase
|
- src/pydase
|
||||||
|
368
poetry.lock
generated
368
poetry.lock
generated
@ -178,6 +178,41 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
|
|||||||
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
|
||||||
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "babel"
|
||||||
|
version = "2.15.0"
|
||||||
|
description = "Internationalization utilities"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"},
|
||||||
|
{file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "beautifulsoup4"
|
||||||
|
version = "4.12.3"
|
||||||
|
description = "Screen-scraping library"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.0"
|
||||||
|
files = [
|
||||||
|
{file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"},
|
||||||
|
{file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
soupsieve = ">1.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
cchardet = ["cchardet"]
|
||||||
|
chardet = ["chardet"]
|
||||||
|
charset-normalizer = ["charset-normalizer"]
|
||||||
|
html5lib = ["html5lib"]
|
||||||
|
lxml = ["lxml"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bidict"
|
name = "bidict"
|
||||||
version = "0.23.1"
|
version = "0.23.1"
|
||||||
@ -189,6 +224,116 @@ files = [
|
|||||||
{file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"},
|
{file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2024.7.4"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
files = [
|
||||||
|
{file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"},
|
||||||
|
{file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.3.2"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.0"
|
||||||
|
files = [
|
||||||
|
{file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"},
|
||||||
|
{file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"},
|
||||||
|
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.7"
|
version = "8.1.7"
|
||||||
@ -979,6 +1124,60 @@ files = [
|
|||||||
dev = ["bump2version (==1.0.1)", "mkdocs (==1.4.0)", "pre-commit", "pytest (==7.1.3)", "pytest-cov (==3.0.0)", "tox"]
|
dev = ["bump2version (==1.0.1)", "mkdocs (==1.4.0)", "pre-commit", "pytest (==7.1.3)", "pytest-cov (==3.0.0)", "tox"]
|
||||||
test = ["mkdocs (==1.4.0)", "pytest (==7.1.3)", "pytest-cov (==3.0.0)"]
|
test = ["mkdocs (==1.4.0)", "pytest (==7.1.3)", "pytest-cov (==3.0.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocs-material"
|
||||||
|
version = "9.5.30"
|
||||||
|
description = "Documentation that simply works"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "mkdocs_material-9.5.30-py3-none-any.whl", hash = "sha256:fc070689c5250a180e9b9d79d8491ef9a3a7acb240db0728728d6c31eeb131d4"},
|
||||||
|
{file = "mkdocs_material-9.5.30.tar.gz", hash = "sha256:3fd417dd42d679e3ba08b9e2d72cd8b8af142cc4a3969676ad6b00993dd182ec"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
babel = ">=2.10,<3.0"
|
||||||
|
colorama = ">=0.4,<1.0"
|
||||||
|
jinja2 = ">=3.0,<4.0"
|
||||||
|
markdown = ">=3.2,<4.0"
|
||||||
|
mkdocs = ">=1.6,<2.0"
|
||||||
|
mkdocs-material-extensions = ">=1.3,<2.0"
|
||||||
|
paginate = ">=0.5,<1.0"
|
||||||
|
pygments = ">=2.16,<3.0"
|
||||||
|
pymdown-extensions = ">=10.2,<11.0"
|
||||||
|
regex = ">=2022.4"
|
||||||
|
requests = ">=2.26,<3.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
|
||||||
|
imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"]
|
||||||
|
recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocs-material-extensions"
|
||||||
|
version = "1.3.1"
|
||||||
|
description = "Extension pack for Python Markdown and MkDocs Material."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"},
|
||||||
|
{file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mkdocs-swagger-ui-tag"
|
||||||
|
version = "0.6.10"
|
||||||
|
description = "A MkDocs plugin supports for add Swagger UI in page."
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "mkdocs-swagger-ui-tag-0.6.10.tar.gz", hash = "sha256:811d55e0905bfecc5f2e743b5b1e4d03b663707d5472984cc7b21f45117dcb29"},
|
||||||
|
{file = "mkdocs_swagger_ui_tag-0.6.10-py3-none-any.whl", hash = "sha256:839373498a42202b3824068064919ad6ddf2e98d5a8deed7d4132719221c0528"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
beautifulsoup4 = ">=4.11.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mkdocstrings"
|
name = "mkdocstrings"
|
||||||
version = "0.22.0"
|
version = "0.22.0"
|
||||||
@ -1236,6 +1435,16 @@ files = [
|
|||||||
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paginate"
|
||||||
|
version = "0.5.6"
|
||||||
|
description = "Divides large result sets into pages for easier browsing"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
files = [
|
||||||
|
{file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@ -1527,15 +1736,29 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.18.0"
|
||||||
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
|
||||||
|
{file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
windows-terminal = ["colorama (>=0.4.6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pymdown-extensions"
|
name = "pymdown-extensions"
|
||||||
version = "10.8.1"
|
version = "10.9"
|
||||||
description = "Extension pack for Python Markdown."
|
description = "Extension pack for Python Markdown."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"},
|
{file = "pymdown_extensions-10.9-py3-none-any.whl", hash = "sha256:d323f7e90d83c86113ee78f3fe62fc9dee5f56b54d912660703ea1816fed5626"},
|
||||||
{file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"},
|
{file = "pymdown_extensions-10.9.tar.gz", hash = "sha256:6ff740bcd99ec4172a938970d42b96128bdc9d4b9bcad72494f29921dc69b753"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -1793,6 +2016,115 @@ files = [
|
|||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "regex"
|
||||||
|
version = "2024.7.24"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b0d3f567fafa0633aee87f08b9276c7062da9616931382993c03808bb68ce"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3426de3b91d1bc73249042742f45c2148803c111d1175b283270177fdf669024"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f273674b445bcb6e4409bf8d1be67bc4b58e8b46fd0d560055d515b8830063cd"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23acc72f0f4e1a9e6e9843d6328177ae3074b4182167e34119ec7233dfeccf53"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65fd3d2e228cae024c411c5ccdffae4c315271eee4a8b839291f84f796b34eca"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c414cbda77dbf13c3bc88b073a1a9f375c7b0cb5e115e15d4b73ec3a2fbc6f59"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf7a89eef64b5455835f5ed30254ec19bf41f7541cd94f266ab7cbd463f00c41"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19c65b00d42804e3fbea9708f0937d157e53429a39b7c61253ff15670ff62cb5"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7a5486ca56c8869070a966321d5ab416ff0f83f30e0e2da1ab48815c8d165d46"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f51f9556785e5a203713f5efd9c085b4a45aecd2a42573e2b5041881b588d1f"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a4997716674d36a82eab3e86f8fa77080a5d8d96a389a61ea1d0e3a94a582cf7"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c0abb5e4e8ce71a61d9446040c1e86d4e6d23f9097275c5bd49ed978755ff0fe"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:18300a1d78cf1290fa583cd8b7cde26ecb73e9f5916690cf9d42de569c89b1ce"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:416c0e4f56308f34cdb18c3f59849479dde5b19febdcd6e6fa4d04b6c31c9faa"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-win32.whl", hash = "sha256:fb168b5924bef397b5ba13aabd8cf5df7d3d93f10218d7b925e360d436863f66"},
|
||||||
|
{file = "regex-2024.7.24-cp310-cp310-win_amd64.whl", hash = "sha256:6b9fc7e9cc983e75e2518496ba1afc524227c163e43d706688a6bb9eca41617e"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:382281306e3adaaa7b8b9ebbb3ffb43358a7bbf585fa93821300a418bb975281"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4fdd1384619f406ad9037fe6b6eaa3de2749e2e12084abc80169e8e075377d3b"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d974d24edb231446f708c455fd08f94c41c1ff4f04bcf06e5f36df5ef50b95a"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2ec4419a3fe6cf8a4795752596dfe0adb4aea40d3683a132bae9c30b81e8d73"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb563dd3aea54c797adf513eeec819c4213d7dbfc311874eb4fd28d10f2ff0f2"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:45104baae8b9f67569f0f1dca5e1f1ed77a54ae1cd8b0b07aba89272710db61e"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:994448ee01864501912abf2bad9203bffc34158e80fe8bfb5b031f4f8e16da51"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fac296f99283ac232d8125be932c5cd7644084a30748fda013028c815ba3364"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e37e809b9303ec3a179085415cb5f418ecf65ec98cdfe34f6a078b46ef823ee"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:01b689e887f612610c869421241e075c02f2e3d1ae93a037cb14f88ab6a8934c"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f6442f0f0ff81775eaa5b05af8a0ffa1dda36e9cf6ec1e0d3d245e8564b684ce"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:871e3ab2838fbcb4e0865a6e01233975df3a15e6fce93b6f99d75cacbd9862d1"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c918b7a1e26b4ab40409820ddccc5d49871a82329640f5005f73572d5eaa9b5e"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-win32.whl", hash = "sha256:2dfbb8baf8ba2c2b9aa2807f44ed272f0913eeeba002478c4577b8d29cde215c"},
|
||||||
|
{file = "regex-2024.7.24-cp311-cp311-win_amd64.whl", hash = "sha256:538d30cd96ed7d1416d3956f94d54e426a8daf7c14527f6e0d6d425fcb4cca52"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fe4ebef608553aff8deb845c7f4f1d0740ff76fa672c011cc0bacb2a00fbde86"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:74007a5b25b7a678459f06559504f1eec2f0f17bca218c9d56f6a0a12bfffdad"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7df9ea48641da022c2a3c9c641650cd09f0cd15e8908bf931ad538f5ca7919c9"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a1141a1dcc32904c47f6846b040275c6e5de0bf73f17d7a409035d55b76f289"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80c811cfcb5c331237d9bad3bea2c391114588cf4131707e84d9493064d267f9"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7214477bf9bd195894cf24005b1e7b496f46833337b5dedb7b2a6e33f66d962c"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d55588cba7553f0b6ec33130bc3e114b355570b45785cebdc9daed8c637dd440"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:558a57cfc32adcf19d3f791f62b5ff564922942e389e3cfdb538a23d65a6b610"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a512eed9dfd4117110b1881ba9a59b31433caed0c4101b361f768e7bcbaf93c5"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:86b17ba823ea76256b1885652e3a141a99a5c4422f4a869189db328321b73799"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5eefee9bfe23f6df09ffb6dfb23809f4d74a78acef004aa904dc7c88b9944b05"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:731fcd76bbdbf225e2eb85b7c38da9633ad3073822f5ab32379381e8c3c12e94"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eaef80eac3b4cfbdd6de53c6e108b4c534c21ae055d1dbea2de6b3b8ff3def38"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-win32.whl", hash = "sha256:185e029368d6f89f36e526764cf12bf8d6f0e3a2a7737da625a76f594bdfcbfc"},
|
||||||
|
{file = "regex-2024.7.24-cp312-cp312-win_amd64.whl", hash = "sha256:2f1baff13cc2521bea83ab2528e7a80cbe0ebb2c6f0bfad15be7da3aed443908"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:66b4c0731a5c81921e938dcf1a88e978264e26e6ac4ec96a4d21ae0354581ae0"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:88ecc3afd7e776967fa16c80f974cb79399ee8dc6c96423321d6f7d4b881c92b"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:64bd50cf16bcc54b274e20235bf8edbb64184a30e1e53873ff8d444e7ac656b2"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eb462f0e346fcf41a901a126b50f8781e9a474d3927930f3490f38a6e73b6950"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a82465ebbc9b1c5c50738536fdfa7cab639a261a99b469c9d4c7dcbb2b3f1e57"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68a8f8c046c6466ac61a36b65bb2395c74451df2ffb8458492ef49900efed293"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac8e84fff5d27420f3c1e879ce9929108e873667ec87e0c8eeb413a5311adfe"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba2537ef2163db9e6ccdbeb6f6424282ae4dea43177402152c67ef869cf3978b"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:43affe33137fcd679bdae93fb25924979517e011f9dea99163f80b82eadc7e53"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:c9bb87fdf2ab2370f21e4d5636e5317775e5d51ff32ebff2cf389f71b9b13750"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:945352286a541406f99b2655c973852da7911b3f4264e010218bbc1cc73168f2"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:8bc593dcce679206b60a538c302d03c29b18e3d862609317cb560e18b66d10cf"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3f3b6ca8eae6d6c75a6cff525c8530c60e909a71a15e1b731723233331de4169"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c51edc3541e11fbe83f0c4d9412ef6c79f664a3745fab261457e84465ec9d5a8"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-win32.whl", hash = "sha256:d0a07763776188b4db4c9c7fb1b8c494049f84659bb387b71c73bbc07f189e96"},
|
||||||
|
{file = "regex-2024.7.24-cp38-cp38-win_amd64.whl", hash = "sha256:8fd5afd101dcf86a270d254364e0e8dddedebe6bd1ab9d5f732f274fa00499a5"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0ffe3f9d430cd37d8fa5632ff6fb36d5b24818c5c986893063b4e5bdb84cdf24"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:25419b70ba00a16abc90ee5fce061228206173231f004437730b67ac77323f0d"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:33e2614a7ce627f0cdf2ad104797d1f68342d967de3695678c0cb84f530709f8"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33a0021893ede5969876052796165bab6006559ab845fd7b515a30abdd990dc"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:04ce29e2c5fedf296b1a1b0acc1724ba93a36fb14031f3abfb7abda2806c1535"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b16582783f44fbca6fcf46f61347340c787d7530d88b4d590a397a47583f31dd"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:836d3cc225b3e8a943d0b02633fb2f28a66e281290302a79df0e1eaa984ff7c1"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:438d9f0f4bc64e8dea78274caa5af971ceff0f8771e1a2333620969936ba10be"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:973335b1624859cb0e52f96062a28aa18f3a5fc77a96e4a3d6d76e29811a0e6e"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c5e69fd3eb0b409432b537fe3c6f44ac089c458ab6b78dcec14478422879ec5f"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:fbf8c2f00904eaf63ff37718eb13acf8e178cb940520e47b2f05027f5bb34ce3"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ae2757ace61bc4061b69af19e4689fa4416e1a04840f33b441034202b5cd02d4"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:44fc61b99035fd9b3b9453f1713234e5a7c92a04f3577252b45feefe1b327759"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:84c312cdf839e8b579f504afcd7b65f35d60b6285d892b19adea16355e8343c9"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-win32.whl", hash = "sha256:ca5b2028c2f7af4e13fb9fc29b28d0ce767c38c7facdf64f6c2cd040413055f1"},
|
||||||
|
{file = "regex-2024.7.24-cp39-cp39-win_amd64.whl", hash = "sha256:7c479f5ae937ec9985ecaf42e2e10631551d909f203e31308c12d703922742f9"},
|
||||||
|
{file = "regex-2024.7.24.tar.gz", hash = "sha256:9cfd009eed1a46b27c14039ad5bbc5e71b6367c5b2e6d5f5da0ea91600817506"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.3"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
|
||||||
|
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = ">=2,<4"
|
||||||
|
idna = ">=2.5,<4"
|
||||||
|
urllib3 = ">=1.21.1,<3"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
|
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -1847,6 +2179,17 @@ files = [
|
|||||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.5"
|
||||||
|
description = "A modern CSS selector implementation for Beautiful Soup."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "soupsieve-2.5-py3-none-any.whl", hash = "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7"},
|
||||||
|
{file = "soupsieve-2.5.tar.gz", hash = "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@ -1891,6 +2234,23 @@ files = [
|
|||||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.2.2"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
|
||||||
|
{file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||||
|
h2 = ["h2 (>=4,<5)"]
|
||||||
|
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "watchdog"
|
name = "watchdog"
|
||||||
version = "4.0.1"
|
version = "4.0.1"
|
||||||
@ -2071,4 +2431,4 @@ multidict = ">=4.0"
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "e5a96d8cc033c893a14aeb90e902923b10884e464b39ccf7a0a08e122a08b21b"
|
content-hash = "80dda5533cf7111fd83407e0cce9228a99af644a6ffab4fa1abe085f4272cabf"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pydase"
|
name = "pydase"
|
||||||
version = "0.8.4"
|
version = "0.8.5"
|
||||||
description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases."
|
description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases."
|
||||||
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
|
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@ -31,15 +31,17 @@ pyright = "^1.1.323"
|
|||||||
pytest-mock = "^3.11.1"
|
pytest-mock = "^3.11.1"
|
||||||
ruff = "^0.2.0"
|
ruff = "^0.2.0"
|
||||||
pytest-asyncio = "^0.23.2"
|
pytest-asyncio = "^0.23.2"
|
||||||
|
requests = "^2.32.3"
|
||||||
|
|
||||||
[tool.poetry.group.docs]
|
[tool.poetry.group.docs]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[tool.poetry.group.docs.dependencies]
|
[tool.poetry.group.docs.dependencies]
|
||||||
mkdocs = "^1.5.2"
|
mkdocs-material = "^9.5.30"
|
||||||
mkdocs-include-markdown-plugin = "^3.9.1"
|
mkdocs-include-markdown-plugin = "^3.9.1"
|
||||||
mkdocstrings = "^0.22.0"
|
mkdocstrings = "^0.22.0"
|
||||||
pymdown-extensions = "^10.1"
|
pymdown-extensions = "^10.1"
|
||||||
|
mkdocs-swagger-ui-tag = "^0.6.10"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
|
@ -216,11 +216,6 @@ class StateManager:
|
|||||||
"readonly": False,
|
"readonly": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
# This will also filter out methods as they are 'read-only'
|
|
||||||
if current_value_dict["readonly"]:
|
|
||||||
logger.debug("Attribute '%s' is read-only. Ignoring new value...", path)
|
|
||||||
return
|
|
||||||
|
|
||||||
if "full_access_path" not in serialized_value:
|
if "full_access_path" not in serialized_value:
|
||||||
# Backwards compatibility for JSON files not containing the
|
# Backwards compatibility for JSON files not containing the
|
||||||
# full_access_path
|
# full_access_path
|
||||||
@ -253,17 +248,7 @@ class StateManager:
|
|||||||
path_parts = parse_full_access_path(path)
|
path_parts = parse_full_access_path(path)
|
||||||
target_obj = get_object_by_path_parts(self.service, path_parts[:-1])
|
target_obj = get_object_by_path_parts(self.service, path_parts[:-1])
|
||||||
|
|
||||||
def cached_value_is_enum(path: str) -> bool:
|
if self.__cached_value_is_enum(path):
|
||||||
try:
|
|
||||||
attr_cache_type = self.cache_manager.get_value_dict_from_cache(path)[
|
|
||||||
"type"
|
|
||||||
]
|
|
||||||
|
|
||||||
return attr_cache_type in ("ColouredEnum", "Enum")
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if cached_value_is_enum(path):
|
|
||||||
enum_attr = get_object_by_path_parts(target_obj, [path_parts[-1]])
|
enum_attr = get_object_by_path_parts(target_obj, [path_parts[-1]])
|
||||||
# take the value of the existing enum class
|
# take the value of the existing enum class
|
||||||
if serialized_value["type"] in ("ColouredEnum", "Enum"):
|
if serialized_value["type"] in ("ColouredEnum", "Enum"):
|
||||||
@ -281,6 +266,15 @@ class StateManager:
|
|||||||
processed_key = parse_serialized_key(path_parts[-1])
|
processed_key = parse_serialized_key(path_parts[-1])
|
||||||
target_obj[processed_key] = value # type: ignore
|
target_obj[processed_key] = value # type: ignore
|
||||||
else:
|
else:
|
||||||
|
# Don't allow adding attributes to objects through state manager
|
||||||
|
if self.__attr_exists_on_target_obj(
|
||||||
|
target_obj=target_obj, name=path_parts[-1]
|
||||||
|
):
|
||||||
|
raise AttributeError(
|
||||||
|
f"{target_obj.__class__.__name__!r} object has no attribute "
|
||||||
|
f"{path_parts[-1]!r}"
|
||||||
|
)
|
||||||
|
|
||||||
setattr(target_obj, path_parts[-1], value)
|
setattr(target_obj, path_parts[-1], value)
|
||||||
|
|
||||||
def __is_loadable_state_attribute(self, full_access_path: str) -> bool:
|
def __is_loadable_state_attribute(self, full_access_path: str) -> bool:
|
||||||
@ -322,3 +316,16 @@ class StateManager:
|
|||||||
path_parts[-1],
|
path_parts[-1],
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def __cached_value_is_enum(self, path: str) -> bool:
|
||||||
|
try:
|
||||||
|
attr_cache_type = self.cache_manager.get_value_dict_from_cache(path)["type"]
|
||||||
|
|
||||||
|
return attr_cache_type in ("ColouredEnum", "Enum")
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def __attr_exists_on_target_obj(self, target_obj: Any, name: str) -> bool:
|
||||||
|
return not is_property_attribute(target_obj, name) and not hasattr(
|
||||||
|
target_obj, name
|
||||||
|
)
|
||||||
|
24
src/pydase/server/web_server/api/__init__.py
Normal file
24
src/pydase/server/web_server/api/__init__.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import aiohttp.web
|
||||||
|
import aiohttp_middlewares.error
|
||||||
|
|
||||||
|
import pydase.server.web_server.api.v1.application
|
||||||
|
from pydase.data_service.state_manager import StateManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def create_api_application(state_manager: StateManager) -> aiohttp.web.Application:
|
||||||
|
api_application = aiohttp.web.Application(
|
||||||
|
middlewares=(aiohttp_middlewares.error.error_middleware(),)
|
||||||
|
)
|
||||||
|
|
||||||
|
api_application.add_subapp(
|
||||||
|
"/v1/",
|
||||||
|
pydase.server.web_server.api.v1.application.create_api_application(
|
||||||
|
state_manager
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return api_application
|
0
src/pydase/server/web_server/api/v1/__init__.py
Normal file
0
src/pydase/server/web_server/api/v1/__init__.py
Normal file
70
src/pydase/server/web_server/api/v1/application.py
Normal file
70
src/pydase/server/web_server/api/v1/application.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import logging
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
import aiohttp.web
|
||||||
|
import aiohttp_middlewares.error
|
||||||
|
|
||||||
|
from pydase.data_service.state_manager import StateManager
|
||||||
|
from pydase.server.web_server.api.v1.endpoints import (
|
||||||
|
get_value,
|
||||||
|
trigger_method,
|
||||||
|
update_value,
|
||||||
|
)
|
||||||
|
from pydase.utils.serialization.serializer import dump
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
API_VERSION = "v1"
|
||||||
|
|
||||||
|
STATUS_OK = 200
|
||||||
|
STATUS_FAILED = 400
|
||||||
|
|
||||||
|
|
||||||
|
def create_api_application(state_manager: StateManager) -> aiohttp.web.Application:
|
||||||
|
api_application = aiohttp.web.Application(
|
||||||
|
middlewares=(aiohttp_middlewares.error.error_middleware(),)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _get_value(request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
logger.info("Handle api request: %s", request)
|
||||||
|
|
||||||
|
access_path = request.rel_url.query["access_path"]
|
||||||
|
|
||||||
|
status = STATUS_OK
|
||||||
|
try:
|
||||||
|
result = get_value(state_manager, access_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
result = dump(e)
|
||||||
|
status = STATUS_FAILED
|
||||||
|
return aiohttp.web.json_response(result, status=status)
|
||||||
|
|
||||||
|
async def _update_value(request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
data: UpdateDict = await request.json()
|
||||||
|
|
||||||
|
try:
|
||||||
|
update_value(state_manager, data)
|
||||||
|
|
||||||
|
return aiohttp.web.json_response()
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return aiohttp.web.json_response(dump(e), status=STATUS_FAILED)
|
||||||
|
|
||||||
|
async def _trigger_method(request: aiohttp.web.Request) -> aiohttp.web.Response:
|
||||||
|
data: TriggerMethodDict = await request.json()
|
||||||
|
|
||||||
|
try:
|
||||||
|
return aiohttp.web.json_response(trigger_method(state_manager, data))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(e)
|
||||||
|
return aiohttp.web.json_response(dump(e), status=STATUS_FAILED)
|
||||||
|
|
||||||
|
api_application.router.add_get("/get_value", _get_value)
|
||||||
|
api_application.router.add_put("/update_value", _update_value)
|
||||||
|
api_application.router.add_put("/trigger_method", _trigger_method)
|
||||||
|
|
||||||
|
return api_application
|
35
src/pydase/server/web_server/api/v1/endpoints.py
Normal file
35
src/pydase/server/web_server/api/v1/endpoints.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydase.data_service.state_manager import StateManager
|
||||||
|
from pydase.server.web_server.sio_setup import TriggerMethodDict, UpdateDict
|
||||||
|
from pydase.utils.helpers import get_object_attr_from_path
|
||||||
|
from pydase.utils.serialization.deserializer import loads
|
||||||
|
from pydase.utils.serialization.serializer import Serializer, dump
|
||||||
|
from pydase.utils.serialization.types import SerializedObject
|
||||||
|
|
||||||
|
|
||||||
|
def update_value(state_manager: StateManager, data: UpdateDict) -> None:
|
||||||
|
path = data["access_path"]
|
||||||
|
|
||||||
|
state_manager.set_service_attribute_value_by_path(
|
||||||
|
path=path, serialized_value=data["value"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_value(state_manager: StateManager, access_path: str) -> SerializedObject:
|
||||||
|
return Serializer.serialize_object(
|
||||||
|
get_object_attr_from_path(state_manager.service, access_path),
|
||||||
|
access_path=access_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_method(state_manager: StateManager, data: TriggerMethodDict) -> Any:
|
||||||
|
method = get_object_attr_from_path(state_manager.service, data["access_path"])
|
||||||
|
|
||||||
|
serialized_args = data.get("args", None)
|
||||||
|
args = loads(serialized_args) if serialized_args else []
|
||||||
|
|
||||||
|
serialized_kwargs = data.get("kwargs", None)
|
||||||
|
kwargs: dict[str, Any] = loads(serialized_kwargs) if serialized_kwargs else {}
|
||||||
|
|
||||||
|
return dump(method(*args, **kwargs))
|
@ -1,15 +1,21 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
|
if sys.version_info < (3, 11):
|
||||||
|
from typing_extensions import NotRequired
|
||||||
|
else:
|
||||||
|
from typing import NotRequired
|
||||||
|
|
||||||
import click
|
import click
|
||||||
import socketio # type: ignore[import-untyped]
|
import socketio # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
import pydase.server.web_server.api.v1.endpoints
|
||||||
import pydase.utils.serialization.deserializer
|
import pydase.utils.serialization.deserializer
|
||||||
import pydase.utils.serialization.serializer
|
import pydase.utils.serialization.serializer
|
||||||
from pydase.data_service.data_service_observer import DataServiceObserver
|
from pydase.data_service.data_service_observer import DataServiceObserver
|
||||||
from pydase.data_service.state_manager import StateManager
|
from pydase.data_service.state_manager import StateManager
|
||||||
from pydase.utils.helpers import get_object_attr_from_path
|
|
||||||
from pydase.utils.logging import SocketIOHandler
|
from pydase.utils.logging import SocketIOHandler
|
||||||
from pydase.utils.serialization.serializer import SerializedObject
|
from pydase.utils.serialization.serializer import SerializedObject
|
||||||
|
|
||||||
@ -39,8 +45,8 @@ class UpdateDict(TypedDict):
|
|||||||
|
|
||||||
class TriggerMethodDict(TypedDict):
|
class TriggerMethodDict(TypedDict):
|
||||||
access_path: str
|
access_path: str
|
||||||
args: SerializedObject
|
args: NotRequired[SerializedObject]
|
||||||
kwargs: SerializedObject
|
kwargs: NotRequired[SerializedObject]
|
||||||
|
|
||||||
|
|
||||||
class RunMethodDict(TypedDict):
|
class RunMethodDict(TypedDict):
|
||||||
@ -137,21 +143,22 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) ->
|
|||||||
return state_manager.cache_manager.cache
|
return state_manager.cache_manager.cache
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
async def update_value(sid: str, data: UpdateDict) -> SerializedObject | None: # type: ignore
|
async def update_value(sid: str, data: UpdateDict) -> SerializedObject | None:
|
||||||
path = data["access_path"]
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
state_manager.set_service_attribute_value_by_path(
|
pydase.server.web_server.api.v1.endpoints.update_value(
|
||||||
path=path, serialized_value=data["value"]
|
state_manager=state_manager, data=data
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return dump(e)
|
return dump(e)
|
||||||
|
return None
|
||||||
|
|
||||||
@sio.event
|
@sio.event
|
||||||
async def get_value(sid: str, access_path: str) -> SerializedObject:
|
async def get_value(sid: str, access_path: str) -> SerializedObject:
|
||||||
try:
|
try:
|
||||||
return state_manager.cache_manager.get_value_dict_from_cache(access_path)
|
return pydase.server.web_server.api.v1.endpoints.get_value(
|
||||||
|
state_manager=state_manager, access_path=access_path
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
return dump(e)
|
return dump(e)
|
||||||
@ -159,17 +166,14 @@ def setup_sio_events(sio: socketio.AsyncServer, state_manager: StateManager) ->
|
|||||||
@sio.event
|
@sio.event
|
||||||
async def trigger_method(sid: str, data: TriggerMethodDict) -> Any:
|
async def trigger_method(sid: str, data: TriggerMethodDict) -> Any:
|
||||||
try:
|
try:
|
||||||
method = get_object_attr_from_path(
|
return pydase.server.web_server.api.v1.endpoints.trigger_method(
|
||||||
state_manager.service, data["access_path"]
|
state_manager=state_manager, data=data
|
||||||
)
|
)
|
||||||
args = loads(data["args"])
|
|
||||||
kwargs: dict[str, Any] = loads(data["kwargs"])
|
|
||||||
return dump(method(*args, **kwargs))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
return dump(e)
|
return dump(e)
|
||||||
|
|
||||||
|
|
||||||
def setup_logging_handler(sio: socketio.AsyncServer) -> None:
|
def setup_logging_handler(sio: socketio.AsyncServer) -> None:
|
||||||
logger = logging.getLogger()
|
logging.getLogger().addHandler(SocketIOHandler(sio))
|
||||||
logger.addHandler(SocketIOHandler(sio))
|
logging.getLogger("pydase").addHandler(SocketIOHandler(sio))
|
||||||
|
@ -9,6 +9,7 @@ import aiohttp_middlewares.cors
|
|||||||
|
|
||||||
from pydase.config import ServiceConfig, WebServerConfig
|
from pydase.config import ServiceConfig, WebServerConfig
|
||||||
from pydase.data_service.data_service_observer import DataServiceObserver
|
from pydase.data_service.data_service_observer import DataServiceObserver
|
||||||
|
from pydase.server.web_server.api import create_api_application
|
||||||
from pydase.server.web_server.sio_setup import (
|
from pydase.server.web_server.sio_setup import (
|
||||||
setup_sio_server,
|
setup_sio_server,
|
||||||
)
|
)
|
||||||
@ -106,6 +107,7 @@ class WebServer:
|
|||||||
app.router.add_get("/service-properties", self._service_properties_route)
|
app.router.add_get("/service-properties", self._service_properties_route)
|
||||||
app.router.add_get("/web-settings", self._web_settings_route)
|
app.router.add_get("/web-settings", self._web_settings_route)
|
||||||
app.router.add_get("/custom.css", self._styles_route)
|
app.router.add_get("/custom.css", self._styles_route)
|
||||||
|
app.add_subapp("/api/", create_api_application(self.state_manager))
|
||||||
|
|
||||||
app.router.add_get(r"/", index)
|
app.router.add_get(r"/", index)
|
||||||
app.router.add_get(r"/{tail:.*}", index)
|
app.router.add_get(r"/{tail:.*}", index)
|
||||||
|
@ -38,6 +38,16 @@ LOGGING_CONFIG = {
|
|||||||
},
|
},
|
||||||
"loggers": {
|
"loggers": {
|
||||||
"pydase": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False},
|
"pydase": {"handlers": ["default"], "level": LOG_LEVEL, "propagate": False},
|
||||||
|
"aiohttp_middlewares": {
|
||||||
|
"handlers": ["default"],
|
||||||
|
"level": logging.WARNING,
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
|
"aiohttp": {
|
||||||
|
"handlers": ["default"],
|
||||||
|
"level": logging.INFO,
|
||||||
|
"propagate": False,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import pytest
|
|||||||
from pydase.client.proxy_loader import ProxyAttributeError
|
from pydase.client.proxy_loader import ProxyAttributeError
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="module")
|
||||||
def pydase_client() -> Generator[pydase.Client, None, Any]:
|
def pydase_client() -> Generator[pydase.Client, None, Any]:
|
||||||
class SubService(pydase.DataService):
|
class SubService(pydase.DataService):
|
||||||
name = "SubService"
|
name = "SubService"
|
||||||
|
@ -6,7 +6,7 @@ import pytest
|
|||||||
from pytest import LogCaptureFixture
|
from pytest import LogCaptureFixture
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="function")
|
||||||
async def test_reconnection(caplog: LogCaptureFixture) -> None:
|
async def test_reconnection(caplog: LogCaptureFixture) -> None:
|
||||||
class MyService(pydase.components.device_connection.DeviceConnection):
|
class MyService(pydase.components.device_connection.DeviceConnection):
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -35,7 +35,7 @@ def test_nested_attributes_cache_callback() -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="function")
|
||||||
async def test_task_status_update() -> None:
|
async def test_task_status_update() -> None:
|
||||||
class ServiceClass(pydase.DataService):
|
class ServiceClass(pydase.DataService):
|
||||||
name = "World"
|
name = "World"
|
||||||
|
@ -10,7 +10,7 @@ from pytest import LogCaptureFixture
|
|||||||
logger = logging.getLogger("pydase")
|
logger = logging.getLogger("pydase")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="function")
|
||||||
async def test_autostart_task_callback(caplog: LogCaptureFixture) -> None:
|
async def test_autostart_task_callback(caplog: LogCaptureFixture) -> None:
|
||||||
class MyService(pydase.DataService):
|
class MyService(pydase.DataService):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
@ -36,7 +36,7 @@ async def test_autostart_task_callback(caplog: LogCaptureFixture) -> None:
|
|||||||
assert "'my_other_task' changed to 'TaskStatus.RUNNING'" in caplog.text
|
assert "'my_other_task' changed to 'TaskStatus.RUNNING'" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="function")
|
||||||
async def test_DataService_subclass_autostart_task_callback(
|
async def test_DataService_subclass_autostart_task_callback(
|
||||||
caplog: LogCaptureFixture,
|
caplog: LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -66,7 +66,7 @@ async def test_DataService_subclass_autostart_task_callback(
|
|||||||
assert "'sub_service.my_other_task' changed to 'TaskStatus.RUNNING'" in caplog.text
|
assert "'sub_service.my_other_task' changed to 'TaskStatus.RUNNING'" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="function")
|
||||||
async def test_DataService_subclass_list_autostart_task_callback(
|
async def test_DataService_subclass_list_autostart_task_callback(
|
||||||
caplog: LogCaptureFixture,
|
caplog: LogCaptureFixture,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -108,7 +108,7 @@ async def test_DataService_subclass_list_autostart_task_callback(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="function")
|
||||||
async def test_start_and_stop_task_methods(caplog: LogCaptureFixture) -> None:
|
async def test_start_and_stop_task_methods(caplog: LogCaptureFixture) -> None:
|
||||||
class MyService(pydase.DataService):
|
class MyService(pydase.DataService):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
|
194
tests/server/web_server/api/v1/test_endpoints.py
Normal file
194
tests/server/web_server/api/v1/test_endpoints.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
import json
|
||||||
|
import threading
|
||||||
|
from collections.abc import Generator
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import pydase
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def pydase_server() -> Generator[None, None, None]:
|
||||||
|
class SubService(pydase.DataService):
|
||||||
|
name = "SubService"
|
||||||
|
|
||||||
|
subservice_instance = SubService()
|
||||||
|
|
||||||
|
class MyService(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._readonly_attr = "MyService"
|
||||||
|
self._my_property = 12.1
|
||||||
|
self.sub_service = SubService()
|
||||||
|
self.list_attr = [1, 2]
|
||||||
|
self.dict_attr = {
|
||||||
|
"foo": subservice_instance,
|
||||||
|
"dotted.key": subservice_instance,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def my_property(self) -> float:
|
||||||
|
return self._my_property
|
||||||
|
|
||||||
|
@my_property.setter
|
||||||
|
def my_property(self, value: float) -> None:
|
||||||
|
self._my_property = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def readonly_attr(self) -> str:
|
||||||
|
return self._readonly_attr
|
||||||
|
|
||||||
|
def my_method(self, input_str: str) -> str:
|
||||||
|
return input_str
|
||||||
|
|
||||||
|
server = pydase.Server(MyService(), web_port=9998)
|
||||||
|
thread = threading.Thread(target=server.run, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"access_path, expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"readonly_attr",
|
||||||
|
{
|
||||||
|
"full_access_path": "readonly_attr",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "str",
|
||||||
|
"value": "MyService",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sub_service.name",
|
||||||
|
{
|
||||||
|
"full_access_path": "sub_service.name",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "str",
|
||||||
|
"value": "SubService",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"list_attr[0]",
|
||||||
|
{
|
||||||
|
"full_access_path": "list_attr[0]",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "int",
|
||||||
|
"value": 1,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'dict_attr["foo"]',
|
||||||
|
{
|
||||||
|
"full_access_path": 'dict_attr["foo"]',
|
||||||
|
"doc": None,
|
||||||
|
"name": "SubService",
|
||||||
|
"readonly": False,
|
||||||
|
"type": "DataService",
|
||||||
|
"value": {
|
||||||
|
"name": {
|
||||||
|
"doc": None,
|
||||||
|
"full_access_path": 'dict_attr["foo"].name',
|
||||||
|
"readonly": False,
|
||||||
|
"type": "str",
|
||||||
|
"value": "SubService",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.asyncio()
|
||||||
|
async def test_get_value(
|
||||||
|
access_path: str,
|
||||||
|
expected: dict[str, Any],
|
||||||
|
pydase_server: None,
|
||||||
|
) -> None:
|
||||||
|
async with aiohttp.ClientSession("http://localhost:9998") as session:
|
||||||
|
resp = await session.get(f"/api/v1/get_value?access_path={access_path}")
|
||||||
|
content = json.loads(await resp.text())
|
||||||
|
assert content == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"access_path, new_value, ok",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"sub_service.name",
|
||||||
|
{
|
||||||
|
"full_access_path": "sub_service.name",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "str",
|
||||||
|
"value": "New Name",
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"list_attr[0]",
|
||||||
|
{
|
||||||
|
"full_access_path": "list_attr[0]",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "int",
|
||||||
|
"value": 11,
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
'dict_attr["foo"].name',
|
||||||
|
{
|
||||||
|
"full_access_path": 'dict_attr["foo"].name',
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "str",
|
||||||
|
"value": "foo name",
|
||||||
|
},
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"readonly_attr",
|
||||||
|
{
|
||||||
|
"full_access_path": "readonly_attr",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": True,
|
||||||
|
"type": "str",
|
||||||
|
"value": "Other Name",
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"invalid_attribute",
|
||||||
|
{
|
||||||
|
"full_access_path": "invalid_attribute",
|
||||||
|
"doc": None,
|
||||||
|
"readonly": False,
|
||||||
|
"type": "float",
|
||||||
|
"value": 12.0,
|
||||||
|
},
|
||||||
|
False,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.asyncio()
|
||||||
|
async def test_update_value(
|
||||||
|
access_path: str,
|
||||||
|
new_value: dict[str, Any],
|
||||||
|
ok: bool,
|
||||||
|
pydase_server: pydase.DataService,
|
||||||
|
) -> None:
|
||||||
|
async with aiohttp.ClientSession("http://localhost:9998") as session:
|
||||||
|
resp = await session.put(
|
||||||
|
"/api/v1/update_value",
|
||||||
|
json={"access_path": access_path, "value": new_value},
|
||||||
|
)
|
||||||
|
assert resp.ok == ok
|
||||||
|
if resp.ok:
|
||||||
|
resp = await session.get(f"/api/v1/get_value?access_path={access_path}")
|
||||||
|
content = json.loads(await resp.text())
|
||||||
|
assert content == new_value
|
@ -207,7 +207,7 @@ def test_ColouredEnum_serialize() -> None:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio(scope="session")
|
@pytest.mark.asyncio(scope="module")
|
||||||
async def test_method_serialization() -> None:
|
async def test_method_serialization() -> None:
|
||||||
class ClassWithMethod(pydase.DataService):
|
class ClassWithMethod(pydase.DataService):
|
||||||
def some_method(self) -> str:
|
def some_method(self) -> str:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user