mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-02 03:50:40 +02:00
Compare commits
59 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
533826a398 | ||
![]() |
982875dee6 | ||
![]() |
e54710cd4d | ||
![]() |
f48f7aacfb | ||
![]() |
e97aab4f36 | ||
![]() |
015c66d5a6 | ||
![]() |
9827d0747c | ||
![]() |
38a12fb72e | ||
![]() |
fb6ec16bf5 | ||
![]() |
9ee498eb5c | ||
![]() |
d015333123 | ||
![]() |
c4e7fe66a8 | ||
![]() |
5f1451a1c1 | ||
![]() |
4c28cbaf7d | ||
![]() |
a97b8eb2b4 | ||
![]() |
f6b5c1b567 | ||
![]() |
f92d525588 | ||
![]() |
61b69d77cc | ||
![]() |
8abe9357cf | ||
![]() |
0dace2a9f0 | ||
![]() |
9992ade0ed | ||
![]() |
6c2cebada2 | ||
![]() |
069a2b4696 | ||
![]() |
38ed8d78de | ||
![]() |
7ff6cab9b3 | ||
![]() |
cbd93fb166 | ||
![]() |
83c30439b6 | ||
![]() |
e596c50915 | ||
![]() |
9920350753 | ||
![]() |
5dec01d800 | ||
![]() |
18c66a8318 | ||
![]() |
3d65240784 | ||
![]() |
0d698e803d | ||
![]() |
02a8791b74 | ||
![]() |
35f658ce4d | ||
![]() |
051e616280 | ||
![]() |
98e9791d09 | ||
![]() |
24ecbd1eb9 | ||
![]() |
cd78d01b04 | ||
![]() |
6be27217cf | ||
![]() |
ae2c99b3ae | ||
![]() |
c32b6a8694 | ||
![]() |
57cfe45c76 | ||
![]() |
7c18d86e9c | ||
![]() |
c5eb5f80b4 | ||
![]() |
18c64db826 | ||
![]() |
27f8e1b1bc | ||
![]() |
f8839f0e71 | ||
![]() |
d31aff0b9b | ||
![]() |
2c1db3fa45 | ||
![]() |
b9cec19b02 | ||
![]() |
6ba5193e9e | ||
![]() |
bc0c69f9e1 | ||
![]() |
b2314f7e33 | ||
![]() |
eb43e7b380 | ||
![]() |
5dc28b0b55 | ||
![]() |
c327215b5f | ||
![]() |
04a3b225f8 | ||
![]() |
86c4514e1a |
2
.github/workflows/python-package.yml
vendored
2
.github/workflows/python-package.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install poetry
|
||||
poetry install --with dev
|
||||
poetry install --with dev --all-extras
|
||||
- name: Check with ruff
|
||||
run: |
|
||||
poetry run ruff check src
|
||||
|
@ -244,6 +244,14 @@ The full documentation provides more detailed information about `pydase`, includ
|
||||
|
||||
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/stable/about/contributing/) for details on how to contribute.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This work was funded by the [ETH Zurich-PSI Quantum Computing Hub](https://www.psi.ch/en/lnq/qchub).
|
||||
|
||||
The main idea behind `pydase` is based on a previous project called `tiqi-plugin`, which
|
||||
was developed within the same research group. While the concept was inspired by that
|
||||
project, `pydase` was implemented from the ground up with a new architecture and design.
|
||||
|
||||
## License
|
||||
|
||||
`pydase` is licensed under the [MIT License][License].
|
||||
|
@ -5,7 +5,7 @@
|
||||
end="<!--getting-started-end-->"
|
||||
%}
|
||||
|
||||
[RESTful API]: ./user-guide/interaction/README.md#restful-api
|
||||
[Python RPC Client]: ./user-guide/interaction/README.md#python-rpc-client
|
||||
[RESTful API]: ./user-guide/interaction/RESTful-API.md
|
||||
[Python RPC Client]: ./user-guide/interaction/Python-Client.md
|
||||
[Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents
|
||||
[Components]: ./user-guide/Components.md
|
||||
|
@ -11,7 +11,7 @@
|
||||
[Defining DataService]: ./getting-started.md#defining-a-dataservice
|
||||
[Web Interface Access]: ./getting-started.md#accessing-the-web-interface
|
||||
[Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client
|
||||
[Customizing Web Interface]: ./user-guide/interaction/README.md#customization-options
|
||||
[Customizing Web Interface]: ./user-guide/interaction/Auto-generated-Frontend.md#customization-options
|
||||
[Task Management]: ./user-guide/Tasks.md
|
||||
[Units]: ./user-guide/Understanding-Units.md
|
||||
[Property Validation]: ./user-guide/Validating-Property-Setters.md
|
||||
|
@ -30,7 +30,7 @@ example of how to separate service code from configuration.
|
||||
|
||||
- **`ENVIRONMENT`**:
|
||||
Defines the operation mode (`"development"` or `"production"`), which influences
|
||||
behaviour such as logging (see [Logging in pydase](https://github.com/tiqi-group/pydase?tab=readme-ov-file#logging-in-pydase)).
|
||||
behaviour such as logging (see [Logging in pydase](./Logging.md)).
|
||||
|
||||
- **`SERVICE_CONFIG_DIR`**:
|
||||
Specifies the directory for configuration files (e.g., `web_settings.json`). Defaults
|
||||
@ -46,8 +46,8 @@ example of how to separate service code from configuration.
|
||||
port. Default: `8001`.
|
||||
|
||||
- **`GENERATE_WEB_SETTINGS`**:
|
||||
When `true`, generates or updates the `web_settings.json` file. Existing entries are
|
||||
preserved, and new entries are appended.
|
||||
When `true`, generates or updates the `web_settings.json` file (see [Tailoring Frontend Component Layout](./interaction/Auto-generated-Frontend.md#tailoring-frontend-component-layout)).
|
||||
Existing entries are preserved, and new entries are appended.
|
||||
|
||||
### Configuring `pydase` via Keyword Arguments
|
||||
|
||||
@ -70,32 +70,32 @@ server = Server(
|
||||
|
||||
## Separating Service Code from Configuration
|
||||
|
||||
To decouple configuration from code, `pydase` utilizes `confz` for configuration
|
||||
management. Below is an example that demonstrates how to configure a `pydase` service
|
||||
To decouple configuration from code, `pydase` utilizes `confz` for configuration
|
||||
management. Below is an example that demonstrates how to configure a `pydase` service
|
||||
for a sensor readout application.
|
||||
|
||||
### Scenario: Configuring a Sensor Service
|
||||
|
||||
Imagine you have multiple sensors distributed across your lab. You need to configure
|
||||
Imagine you have multiple sensors distributed across your lab. You need to configure
|
||||
each service instance with:
|
||||
|
||||
1. **Hostname**: The hostname or IP address of the sensor.
|
||||
2. **Authentication Token**: A token or credentials to authenticate with the sensor.
|
||||
3. **Readout Interval**: A periodic interval to read sensor data and log it to a
|
||||
3. **Readout Interval**: A periodic interval to read sensor data and log it to a
|
||||
database.
|
||||
|
||||
Given the repository structure:
|
||||
|
||||
```bash title="Service Repository Structure"
|
||||
my_sensor
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
└── src
|
||||
└── my_sensor
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
└── src
|
||||
└── my_sensor
|
||||
├── my_sensor.py
|
||||
├── config.py
|
||||
├── __init__.py
|
||||
└── __main__.py
|
||||
├── config.py
|
||||
├── __init__.py
|
||||
└── __main__.py
|
||||
```
|
||||
|
||||
Your service might look like this:
|
||||
@ -119,7 +119,7 @@ class MySensorConfig(confz.BaseConfig):
|
||||
|
||||
This class defines configurable parameters and loads values from a `config.yaml` file
|
||||
located in the service’s configuration directory (which is configurable through an
|
||||
environment variable, see [above](#configuring-pydase-using-environment-variables)).
|
||||
environment variable, see [above](#configuring-pydase-using-environment-variables)).
|
||||
A sample YAML file might look like this:
|
||||
|
||||
```yaml title="config.yaml"
|
||||
|
48
docs/user-guide/advanced/SOCKS-Proxy.md
Normal file
48
docs/user-guide/advanced/SOCKS-Proxy.md
Normal file
@ -0,0 +1,48 @@
|
||||
# Connecting Through a SOCKS5 Proxy
|
||||
|
||||
If your target service is only reachable via an SSH gateway or resides behind a
|
||||
firewall, you can route your [`pydase.Client`][pydase.Client] connection through a local
|
||||
SOCKS5 proxy. This is particularly useful in network environments where direct access to
|
||||
the service is not possible.
|
||||
|
||||
## Setting Up a SOCKS5 Proxy
|
||||
|
||||
You can create a local [SOCKS5 proxy](https://en.wikipedia.org/wiki/SOCKS) using SSH's
|
||||
`-D` option:
|
||||
|
||||
```bash
|
||||
ssh -D 2222 user@gateway.example.com
|
||||
```
|
||||
|
||||
This command sets up a SOCKS5 proxy on `localhost:2222`, securely forwarding traffic
|
||||
over the SSH connection.
|
||||
|
||||
## Using the Proxy in Your Python Client
|
||||
|
||||
Once the proxy is running, configure the [`pydase.Client`][pydase.Client] to route
|
||||
traffic through it using the `proxy_url` parameter:
|
||||
|
||||
```python
|
||||
import pydase
|
||||
|
||||
client = pydase.Client(
|
||||
url="ws://target-service:8001",
|
||||
proxy_url="socks5://localhost:2222"
|
||||
).proxy
|
||||
```
|
||||
|
||||
* You can also use this setup with `wss://` URLs for encrypted WebSocket connections.
|
||||
|
||||
## Installing Required Dependencies
|
||||
|
||||
To use this feature, you must install the optional `socks` dependency group, which
|
||||
includes [`aiohttp_socks`](https://pypi.org/project/aiohttp-socks/):
|
||||
|
||||
- `poetry`
|
||||
```bash
|
||||
poetry add "pydase[socks]"
|
||||
```
|
||||
- `pip`
|
||||
```bash
|
||||
pip install "pydase[socks]"
|
||||
```
|
@ -89,7 +89,7 @@ Each key in the file corresponds to the full access path of public attributes, p
|
||||
- **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 the `SERVICE_CONFIG_DIR` environment variable. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](../Configuration).
|
||||
The `web_settings.json` file will be stored in the directory specified by the `SERVICE_CONFIG_DIR` environment variable. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](../Configuration.md).
|
||||
|
||||
For example, styling the following service
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Python RPC Client
|
||||
|
||||
The [`pydase.Client`][pydase.Client] allows you to connect to a remote `pydase` service using socket.io, facilitating interaction with the service as though it were running locally.
|
||||
The [`pydase.Client`][pydase.Client] allows you to connect to a remote `pydase` service using Socket.IO, facilitating interaction with the service as though it were running locally.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
@ -9,6 +9,7 @@ import pydase
|
||||
|
||||
# Replace <ip_addr> and <service_port> with the appropriate values for your service
|
||||
client_proxy = pydase.Client(url="ws://<ip_addr>:<service_port>").proxy
|
||||
|
||||
# For SSL-encrypted services, use the wss protocol
|
||||
# client_proxy = pydase.Client(url="wss://your-domain.ch").proxy
|
||||
|
||||
@ -22,6 +23,12 @@ The proxy acts as a local representation of the remote service, enabling intuiti
|
||||
|
||||
The proxy class automatically synchronizes with the server's attributes and methods, keeping itself up-to-date with any changes. This dynamic synchronization essentially mirrors the server's API, making it feel like you're working with a local object.
|
||||
|
||||
### Accessing Services Behind Firewalls or SSH Gateways
|
||||
|
||||
If your service is only reachable through a private network or SSH gateway, you can route your connection through a local SOCKS5 proxy using the `proxy_url` parameter.
|
||||
|
||||
See [Connecting Through a SOCKS5 Proxy](../advanced/SOCKS-Proxy.md) for details.
|
||||
|
||||
## Context Manager Support
|
||||
|
||||
You can also use the client within a context manager, which automatically handles connection management (i.e., opening and closing the connection):
|
||||
@ -51,8 +58,9 @@ class MyService(pydase.DataService):
|
||||
proxy = pydase.Client(
|
||||
url="ws://<ip_addr>:<service_port>",
|
||||
block_until_connected=False,
|
||||
client_id="my_pydase_client_id",
|
||||
client_id="my_pydase_client_id", # optional, defaults to system hostname
|
||||
).proxy
|
||||
|
||||
# For SSL-encrypted services, use the wss protocol
|
||||
# proxy = pydase.Client(
|
||||
# url="wss://your-domain.ch",
|
||||
@ -68,12 +76,12 @@ if __name__ == "__main__":
|
||||
|
||||
In this example:
|
||||
- The `MyService` class has a `proxy` attribute that connects to a `pydase` service at `<ip_addr>:<service_port>`.
|
||||
- By setting `block_until_connected=False`, the service can start without waiting for the connection to succeed, which is particularly useful in distributed systems where services may initialize in any order.
|
||||
- By setting `client_id`, the server will provide more accurate logs of the connecting client. If set, this ID is sent as `X-Client-Id` header in the HTTP(s) request.
|
||||
- By setting `block_until_connected=False`, the service can start without waiting for the connection to succeed.
|
||||
- The `client_id` is optional. If not specified, it defaults to the system hostname, which will be sent in the `X-Client-Id` HTTP header for logging or authentication on the server side.
|
||||
|
||||
## Custom `socketio.AsyncClient` Connection Parameters
|
||||
|
||||
You can also configure advanced connection options by passing additional arguments to the underlying [`AsyncClient`][socketio.AsyncClient] via `sio_client_kwargs`. This allows you to fine-tune reconnection behaviour, delays, and other settings:
|
||||
You can configure advanced connection options by passing arguments to the underlying [`AsyncClient`][socketio.AsyncClient] via `sio_client_kwargs`. For example:
|
||||
|
||||
```python
|
||||
client = pydase.Client(
|
@ -1,81 +1,7 @@
|
||||
# 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.
|
||||
`pydase` offers multiple ways for users to interact with the services they create.
|
||||
|
||||
{%
|
||||
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 -->
|
||||
- [Auto-generated Frontend](./Auto-generated-Frontend.md)
|
||||
- [RESTful API](./RESTful-API.md)
|
||||
- [Python Client](./Python-Client.md)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Form, InputGroup } from "react-bootstrap";
|
||||
import { DocStringComponent } from "./DocStringComponent";
|
||||
import "../App.css";
|
||||
@ -175,6 +175,33 @@ const handleNumericKey = (
|
||||
return { value: newValue, selectionStart: selectionStart + 1 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the new cursor position after moving left by a specified step size.
|
||||
*
|
||||
* @param cursorPosition - The current position of the cursor.
|
||||
* @param step - The number of positions to move left.
|
||||
* @returns The new cursor position, clamped to a minimum of 0.
|
||||
*/
|
||||
const getCursorLeftPosition = (cursorPosition: number, step: number): number => {
|
||||
return Math.max(0, cursorPosition - step);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the new cursor position after moving right by a specified step size.
|
||||
*
|
||||
* @param cursorPosition - The current position of the cursor.
|
||||
* @param step - The number of positions to move right.
|
||||
* @param maxPosition - The maximum allowed cursor position (e.g., value.length).
|
||||
* @returns The new cursor position, clamped to a maximum of maxPosition.
|
||||
*/
|
||||
const getCursorRightPosition = (
|
||||
cursorPosition: number,
|
||||
step: number,
|
||||
maxPosition: number,
|
||||
): number => {
|
||||
return Math.min(maxPosition, cursorPosition + step);
|
||||
};
|
||||
|
||||
export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
const {
|
||||
fullAccessPath,
|
||||
@ -191,7 +218,8 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
} = props;
|
||||
|
||||
// Create a state for the cursor position
|
||||
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
|
||||
const cursorPositionRef = useRef<number | null>(null);
|
||||
|
||||
// Create a state for the input string
|
||||
const [inputString, setInputString] = useState(value.toString());
|
||||
const renderCount = useRenderCount();
|
||||
@ -200,26 +228,40 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
const { key, target } = event;
|
||||
|
||||
const inputTarget = target as HTMLInputElement;
|
||||
if (key === "F1" || key === "F5" || key === "F12" || key === "Tab") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Get the current input value and cursor position
|
||||
const { value } = inputTarget;
|
||||
const valueLength = value.length;
|
||||
const selectionEnd = inputTarget.selectionEnd ?? 0;
|
||||
let selectionStart = inputTarget.selectionStart ?? 0;
|
||||
|
||||
if (key === "F1" || key === "F5" || key === "F12" || key === "Tab") {
|
||||
return;
|
||||
} else if (key === "ArrowLeft" || key === "ArrowRight") {
|
||||
const hasSelection = selectionEnd > selectionStart;
|
||||
|
||||
if (hasSelection && !event.shiftKey) {
|
||||
// Collapse selection: ArrowLeft -> start, ArrowRight -> end
|
||||
const collapseTo = key === "ArrowLeft" ? selectionStart : selectionEnd;
|
||||
cursorPositionRef.current = collapseTo;
|
||||
} else {
|
||||
// No selection or shift key is pressed, just move cursor by one
|
||||
const newSelectionStart =
|
||||
key === "ArrowLeft"
|
||||
? getCursorLeftPosition(selectionStart, 1)
|
||||
: getCursorRightPosition(selectionEnd, 1, valueLength);
|
||||
|
||||
cursorPositionRef.current = newSelectionStart;
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
let newValue: string = value;
|
||||
if (event.ctrlKey && key === "a") {
|
||||
// Select everything when pressing Ctrl + a
|
||||
inputTarget.setSelectionRange(0, value.length);
|
||||
return;
|
||||
} else if (key === "ArrowRight" || key === "ArrowLeft") {
|
||||
// Move the cursor with the arrow keys and store its position
|
||||
selectionStart = key === "ArrowRight" ? selectionStart + 1 : selectionStart - 1;
|
||||
setCursorPosition(selectionStart);
|
||||
return;
|
||||
} else if ((key >= "0" && key <= "9") || key === "-") {
|
||||
// Check if a number key or a decimal point key is pressed
|
||||
({ value: newValue, selectionStart } = handleNumericKey(
|
||||
@ -314,7 +356,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
setInputString(newValue);
|
||||
|
||||
// Save the current cursor position before the component re-renders
|
||||
setCursorPosition(selectionStart);
|
||||
cursorPositionRef.current = selectionStart;
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
@ -367,8 +409,11 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
useEffect(() => {
|
||||
// Set the cursor position after the component re-renders
|
||||
const inputElement = document.getElementsByName(id)[0] as HTMLInputElement;
|
||||
if (inputElement && cursorPosition !== null) {
|
||||
inputElement.setSelectionRange(cursorPosition, cursorPosition);
|
||||
if (inputElement && cursorPositionRef.current !== null) {
|
||||
inputElement.setSelectionRange(
|
||||
cursorPositionRef.current,
|
||||
cursorPositionRef.current,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -6,7 +6,11 @@ nav:
|
||||
- Getting Started: getting-started.md
|
||||
- User Guide:
|
||||
- Components Guide: user-guide/Components.md
|
||||
- Interacting with pydase Services: user-guide/interaction/README.md
|
||||
- Interaction:
|
||||
- Overview: user-guide/interaction/README.md
|
||||
- Auto-generated Frontend: user-guide/interaction/Auto-generated-Frontend.md
|
||||
- RESTful API: user-guide/interaction/RESTful-API.md
|
||||
- Python Client: user-guide/interaction/Python-Client.md
|
||||
- Achieving Service Persistence: user-guide/Service_Persistence.md
|
||||
- Understanding Tasks: user-guide/Tasks.md
|
||||
- Understanding Units: user-guide/Understanding-Units.md
|
||||
@ -15,6 +19,7 @@ nav:
|
||||
- Logging in pydase: user-guide/Logging.md
|
||||
- Advanced:
|
||||
- Deploying behind a Reverse Proxy: user-guide/advanced/Reverse-Proxy.md
|
||||
- Connecting through a SOCKS Proxy: user-guide/advanced/SOCKS-Proxy.md
|
||||
- Developer Guide:
|
||||
- Developer Guide: dev-guide/README.md
|
||||
- API Reference: dev-guide/api.md
|
||||
|
440
poetry.lock
generated
440
poetry.lock
generated
@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiohappyeyeballs"
|
||||
@ -14,93 +14,93 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
version = "3.11.16"
|
||||
version = "3.11.18"
|
||||
description = "Async http client/server framework (asyncio)"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb46bb0f24813e6cede6cc07b1961d4b04f331f7112a23b5e21f567da4ee50aa"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:54eb3aead72a5c19fad07219acd882c1643a1027fbcdefac9b502c267242f955"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:38bea84ee4fe24ebcc8edeb7b54bf20f06fd53ce4d2cc8b74344c5b9620597fd"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0666afbe984f6933fe72cd1f1c3560d8c55880a0bdd728ad774006eb4241ecd"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ba92a2d9ace559a0a14b03d87f47e021e4fa7681dc6970ebbc7b447c7d4b7cd"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ad1d59fd7114e6a08c4814983bb498f391c699f3c78712770077518cae63ff7"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b88a2bf26965f2015a771381624dd4b0839034b70d406dc74fd8be4cc053e3"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:576f5ca28d1b3276026f7df3ec841ae460e0fc3aac2a47cbf72eabcfc0f102e1"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a2a450bcce4931b295fc0848f384834c3f9b00edfc2150baafb4488c27953de6"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:37dcee4906454ae377be5937ab2a66a9a88377b11dd7c072df7a7c142b63c37c"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d0c970c0d602b1017e2067ff3b7dac41c98fef4f7472ec2ea26fd8a4e8c2149"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:004511d3413737700835e949433536a2fe95a7d0297edd911a1e9705c5b5ea43"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c15b2271c44da77ee9d822552201180779e5e942f3a71fb74e026bf6172ff287"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad9509ffb2396483ceacb1eee9134724443ee45b92141105a4645857244aecc8"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-win32.whl", hash = "sha256:634d96869be6c4dc232fc503e03e40c42d32cfaa51712aee181e922e61d74814"},
|
||||
{file = "aiohttp-3.11.16-cp310-cp310-win_amd64.whl", hash = "sha256:938f756c2b9374bbcc262a37eea521d8a0e6458162f2a9c26329cc87fdf06534"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8cb0688a8d81c63d716e867d59a9ccc389e97ac7037ebef904c2b89334407180"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ad1fb47da60ae1ddfb316f0ff16d1f3b8e844d1a1e154641928ea0583d486ed"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df7db76400bf46ec6a0a73192b14c8295bdb9812053f4fe53f4e789f3ea66bbb"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc3a145479a76ad0ed646434d09216d33d08eef0d8c9a11f5ae5cdc37caa3540"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d007aa39a52d62373bd23428ba4a2546eed0e7643d7bf2e41ddcefd54519842c"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6ddd90d9fb4b501c97a4458f1c1720e42432c26cb76d28177c5b5ad4e332601"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a2f451849e6b39e5c226803dcacfa9c7133e9825dcefd2f4e837a2ec5a3bb98"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8df6612df74409080575dca38a5237282865408016e65636a76a2eb9348c2567"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:78e6e23b954644737e385befa0deb20233e2dfddf95dd11e9db752bdd2a294d3"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:696ef00e8a1f0cec5e30640e64eca75d8e777933d1438f4facc9c0cdf288a810"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3538bc9fe1b902bef51372462e3d7c96fce2b566642512138a480b7adc9d508"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3ab3367bb7f61ad18793fea2ef71f2d181c528c87948638366bf1de26e239183"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:56a3443aca82abda0e07be2e1ecb76a050714faf2be84256dae291182ba59049"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:61c721764e41af907c9d16b6daa05a458f066015abd35923051be8705108ed17"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-win32.whl", hash = "sha256:3e061b09f6fa42997cf627307f220315e313ece74907d35776ec4373ed718b86"},
|
||||
{file = "aiohttp-3.11.16-cp311-cp311-win_amd64.whl", hash = "sha256:745f1ed5e2c687baefc3c5e7b4304e91bf3e2f32834d07baaee243e349624b24"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:911a6e91d08bb2c72938bc17f0a2d97864c531536b7832abee6429d5296e5b27"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac13b71761e49d5f9e4d05d33683bbafef753e876e8e5a7ef26e937dd766713"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fd36c119c5d6551bce374fcb5c19269638f8d09862445f85a5a48596fd59f4bb"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d489d9778522fbd0f8d6a5c6e48e3514f11be81cb0a5954bdda06f7e1594b321"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69a2cbd61788d26f8f1e626e188044834f37f6ae3f937bd9f08b65fc9d7e514e"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd464ba806e27ee24a91362ba3621bfc39dbbb8b79f2e1340201615197370f7c"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ce63ae04719513dd2651202352a2beb9f67f55cb8490c40f056cea3c5c355ce"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b00dd520d88eac9d1768439a59ab3d145065c91a8fab97f900d1b5f802895e"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f6428fee52d2bcf96a8aa7b62095b190ee341ab0e6b1bcf50c615d7966fd45b"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:13ceac2c5cdcc3f64b9015710221ddf81c900c5febc505dbd8f810e770011540"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fadbb8f1d4140825069db3fedbbb843290fd5f5bc0a5dbd7eaf81d91bf1b003b"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:6a792ce34b999fbe04a7a71a90c74f10c57ae4c51f65461a411faa70e154154e"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f4065145bf69de124accdd17ea5f4dc770da0a6a6e440c53f6e0a8c27b3e635c"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fa73e8c2656a3653ae6c307b3f4e878a21f87859a9afab228280ddccd7369d71"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-win32.whl", hash = "sha256:f244b8e541f414664889e2c87cac11a07b918cb4b540c36f7ada7bfa76571ea2"},
|
||||
{file = "aiohttp-3.11.16-cp312-cp312-win_amd64.whl", hash = "sha256:23a15727fbfccab973343b6d1b7181bfb0b4aa7ae280f36fd2f90f5476805682"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a3814760a1a700f3cfd2f977249f1032301d0a12c92aba74605cfa6ce9f78489"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9b751a6306f330801665ae69270a8a3993654a85569b3469662efaad6cf5cc50"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ad497f38a0d6c329cb621774788583ee12321863cd4bd9feee1effd60f2ad133"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca37057625693d097543bd88076ceebeb248291df9d6ca8481349efc0b05dcd0"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5abcbba9f4b463a45c8ca8b7720891200658f6f46894f79517e6cd11f3405ca"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f420bfe862fb357a6d76f2065447ef6f484bc489292ac91e29bc65d2d7a2c84d"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58ede86453a6cf2d6ce40ef0ca15481677a66950e73b0a788917916f7e35a0bb"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fdec0213244c39973674ca2a7f5435bf74369e7d4e104d6c7473c81c9bcc8c4"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:72b1b03fb4655c1960403c131740755ec19c5898c82abd3961c364c2afd59fe7"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:780df0d837276276226a1ff803f8d0fa5f8996c479aeef52eb040179f3156cbd"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ecdb8173e6c7aa09eee342ac62e193e6904923bd232e76b4157ac0bfa670609f"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6db7458ab89c7d80bc1f4e930cc9df6edee2200127cfa6f6e080cf619eddfbd"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2540ddc83cc724b13d1838026f6a5ad178510953302a49e6d647f6e1de82bc34"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3b4e6db8dc4879015b9955778cfb9881897339c8fab7b3676f8433f849425913"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-win32.whl", hash = "sha256:493910ceb2764f792db4dc6e8e4b375dae1b08f72e18e8f10f18b34ca17d0979"},
|
||||
{file = "aiohttp-3.11.16-cp313-cp313-win_amd64.whl", hash = "sha256:42864e70a248f5f6a49fdaf417d9bc62d6e4d8ee9695b24c5916cb4bb666c802"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bbcba75fe879ad6fd2e0d6a8d937f34a571f116a0e4db37df8079e738ea95c71"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:87a6e922b2b2401e0b0cf6b976b97f11ec7f136bfed445e16384fbf6fd5e8602"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccf10f16ab498d20e28bc2b5c1306e9c1512f2840f7b6a67000a517a4b37d5ee"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb3d0cc5cdb926090748ea60172fa8a213cec728bd6c54eae18b96040fcd6227"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d07502cc14ecd64f52b2a74ebbc106893d9a9717120057ea9ea1fd6568a747e7"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:776c8e959a01e5e8321f1dec77964cb6101020a69d5a94cd3d34db6d555e01f7"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0902e887b0e1d50424112f200eb9ae3dfed6c0d0a19fc60f633ae5a57c809656"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e87fd812899aa78252866ae03a048e77bd11b80fb4878ce27c23cade239b42b2"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0a950c2eb8ff17361abd8c85987fd6076d9f47d040ebffce67dce4993285e973"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:c10d85e81d0b9ef87970ecbdbfaeec14a361a7fa947118817fcea8e45335fa46"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:7951decace76a9271a1ef181b04aa77d3cc309a02a51d73826039003210bdc86"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14461157d8426bcb40bd94deb0450a6fa16f05129f7da546090cebf8f3123b0f"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9756d9b9d4547e091f99d554fbba0d2a920aab98caa82a8fb3d3d9bee3c9ae85"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:87944bd16b7fe6160607f6a17808abd25f17f61ae1e26c47a491b970fb66d8cb"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-win32.whl", hash = "sha256:92b7ee222e2b903e0a4b329a9943d432b3767f2d5029dbe4ca59fb75223bbe2e"},
|
||||
{file = "aiohttp-3.11.16-cp39-cp39-win_amd64.whl", hash = "sha256:17ae4664031aadfbcb34fd40ffd90976671fa0c0286e6c4113989f78bebab37a"},
|
||||
{file = "aiohttp-3.11.16.tar.gz", hash = "sha256:16f8a2c9538c14a557b4d309ed4d0a7c60f0253e8ed7b6c9a2859a7582f8b1b8"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:96264854fedbea933a9ca4b7e0c745728f01380691687b7365d18d9e977179c4"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9602044ff047043430452bc3a2089743fa85da829e6fc9ee0025351d66c332b6"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5691dc38750fcb96a33ceef89642f139aa315c8a193bbd42a0c33476fd4a1609"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:554c918ec43f8480b47a5ca758e10e793bd7410b83701676a4782672d670da55"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a4076a2b3ba5b004b8cffca6afe18a3b2c5c9ef679b4d1e9859cf76295f8d4f"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:767a97e6900edd11c762be96d82d13a1d7c4fc4b329f054e88b57cdc21fded94"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0ddc9337a0fb0e727785ad4f41163cc314376e82b31846d3835673786420ef1"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f414f37b244f2a97e79b98d48c5ff0789a0b4b4609b17d64fa81771ad780e415"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdb239f47328581e2ec7744ab5911f97afb10752332a6dd3d98e14e429e1a9e7"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f2c50bad73ed629cc326cc0f75aed8ecfb013f88c5af116f33df556ed47143eb"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a8d8f20c39d3fa84d1c28cdb97f3111387e48209e224408e75f29c6f8e0861d"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:106032eaf9e62fd6bc6578c8b9e6dc4f5ed9a5c1c7fb2231010a1b4304393421"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:b491e42183e8fcc9901d8dcd8ae644ff785590f1727f76ca86e731c61bfe6643"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ad8c745ff9460a16b710e58e06a9dec11ebc0d8f4dd82091cefb579844d69868"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-win32.whl", hash = "sha256:8e57da93e24303a883146510a434f0faf2f1e7e659f3041abc4e3fb3f6702a9f"},
|
||||
{file = "aiohttp-3.11.18-cp310-cp310-win_amd64.whl", hash = "sha256:cc93a4121d87d9f12739fc8fab0a95f78444e571ed63e40bfc78cd5abe700ac9"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:427fdc56ccb6901ff8088544bde47084845ea81591deb16f957897f0f0ba1be9"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c828b6d23b984255b85b9b04a5b963a74278b7356a7de84fda5e3b76866597b"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c2eaa145bb36b33af1ff2860820ba0589e165be4ab63a49aebfd0981c173b66"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d518ce32179f7e2096bf4e3e8438cf445f05fedd597f252de9f54c728574756"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700055a6e05c2f4711011a44364020d7a10fbbcd02fbf3e30e8f7e7fddc8717"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8bd1cde83e4684324e6ee19adfc25fd649d04078179890be7b29f76b501de8e4"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73b8870fe1c9a201b8c0d12c94fe781b918664766728783241a79e0468427e4f"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25557982dd36b9e32c0a3357f30804e80790ec2c4d20ac6bcc598533e04c6361"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7e889c9df381a2433802991288a61e5a19ceb4f61bd14f5c9fa165655dcb1fd1"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9ea345fda05bae217b6cce2acf3682ce3b13d0d16dd47d0de7080e5e21362421"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9f26545b9940c4b46f0a9388fd04ee3ad7064c4017b5a334dd450f616396590e"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3a621d85e85dccabd700294494d7179ed1590b6d07a35709bb9bd608c7f5dd1d"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9c23fd8d08eb9c2af3faeedc8c56e134acdaf36e2117ee059d7defa655130e5f"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9e6b0e519067caa4fd7fb72e3e8002d16a68e84e62e7291092a5433763dc0dd"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-win32.whl", hash = "sha256:122f3e739f6607e5e4c6a2f8562a6f476192a682a52bda8b4c6d4254e1138f4d"},
|
||||
{file = "aiohttp-3.11.18-cp311-cp311-win_amd64.whl", hash = "sha256:e6f3c0a3a1e73e88af384b2e8a0b9f4fb73245afd47589df2afcab6b638fa0e6"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:63d71eceb9cad35d47d71f78edac41fcd01ff10cacaa64e473d1aec13fa02df2"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d1929da615840969929e8878d7951b31afe0bac883d84418f92e5755d7b49508"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d0aebeb2392f19b184e3fdd9e651b0e39cd0f195cdb93328bd124a1d455cd0e"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3849ead845e8444f7331c284132ab314b4dac43bfae1e3cf350906d4fff4620f"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e8452ad6b2863709f8b3d615955aa0807bc093c34b8e25b3b52097fe421cb7f"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b8d2b42073611c860a37f718b3d61ae8b4c2b124b2e776e2c10619d920350ec"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fbf91f6a0ac317c0a07eb328a1384941872f6761f2e6f7208b63c4cc0a7ff6"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ff5625413fec55216da5eaa011cf6b0a2ed67a565914a212a51aa3755b0009"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7f33a92a2fde08e8c6b0c61815521324fc1612f397abf96eed86b8e31618fdb4"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:11d5391946605f445ddafda5eab11caf310f90cdda1fd99865564e3164f5cff9"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3cc314245deb311364884e44242e00c18b5896e4fe6d5f942e7ad7e4cb640adb"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0f421843b0f70740772228b9e8093289924359d306530bcd3926f39acbe1adda"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:e220e7562467dc8d589e31c1acd13438d82c03d7f385c9cd41a3f6d1d15807c1"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ab2ef72f8605046115bc9aa8e9d14fd49086d405855f40b79ed9e5c1f9f4faea"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-win32.whl", hash = "sha256:12a62691eb5aac58d65200c7ae94d73e8a65c331c3a86a2e9670927e94339ee8"},
|
||||
{file = "aiohttp-3.11.18-cp312-cp312-win_amd64.whl", hash = "sha256:364329f319c499128fd5cd2d1c31c44f234c58f9b96cc57f743d16ec4f3238c8"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:474215ec618974054cf5dc465497ae9708543cbfc312c65212325d4212525811"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ced70adf03920d4e67c373fd692123e34d3ac81dfa1c27e45904a628567d804"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d9f6c0152f8d71361905aaf9ed979259537981f47ad099c8b3d81e0319814bd"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a35197013ed929c0aed5c9096de1fc5a9d336914d73ab3f9df14741668c0616c"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:540b8a1f3a424f1af63e0af2d2853a759242a1769f9f1ab053996a392bd70118"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9e6710ebebfce2ba21cee6d91e7452d1125100f41b906fb5af3da8c78b764c1"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8af2ef3b4b652ff109f98087242e2ab974b2b2b496304063585e3d78de0b000"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:28c3f975e5ae3dbcbe95b7e3dcd30e51da561a0a0f2cfbcdea30fc1308d72137"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c28875e316c7b4c3e745172d882d8a5c835b11018e33432d281211af35794a93"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:13cd38515568ae230e1ef6919e2e33da5d0f46862943fcda74e7e915096815f3"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0e2a92101efb9f4c2942252c69c63ddb26d20f46f540c239ccfa5af865197bb8"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e6d3e32b8753c8d45ac550b11a1090dd66d110d4ef805ffe60fa61495360b3b2"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ea4cf2488156e0f281f93cc2fd365025efcba3e2d217cbe3df2840f8c73db261"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d4df95ad522c53f2b9ebc07f12ccd2cb15550941e11a5bbc5ddca2ca56316d7"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-win32.whl", hash = "sha256:cdd1bbaf1e61f0d94aced116d6e95fe25942f7a5f42382195fd9501089db5d78"},
|
||||
{file = "aiohttp-3.11.18-cp313-cp313-win_amd64.whl", hash = "sha256:bdd619c27e44382cf642223f11cfd4d795161362a5a1fc1fa3940397bc89db01"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:469ac32375d9a716da49817cd26f1916ec787fc82b151c1c832f58420e6d3533"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3cec21dd68924179258ae14af9f5418c1ebdbba60b98c667815891293902e5e0"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b426495fb9140e75719b3ae70a5e8dd3a79def0ae3c6c27e012fc59f16544a4a"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad2f41203e2808616292db5d7170cccf0c9f9c982d02544443c7eb0296e8b0c7"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc0ae0a5e9939e423e065a3e5b00b24b8379f1db46046d7ab71753dfc7dd0e1"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe7cdd3f7d1df43200e1c80f1aed86bb36033bf65e3c7cf46a2b97a253ef8798"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5199be2a2f01ffdfa8c3a6f5981205242986b9e63eb8ae03fd18f736e4840721"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ccec9e72660b10f8e283e91aa0295975c7bd85c204011d9f5eb69310555cf30"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1596ebf17e42e293cbacc7a24c3e0dc0f8f755b40aff0402cb74c1ff6baec1d3"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:eab7b040a8a873020113ba814b7db7fa935235e4cbaf8f3da17671baa1024863"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5d61df4a05476ff891cff0030329fee4088d40e4dc9b013fac01bc3c745542c2"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:46533e6792e1410f9801d09fd40cbbff3f3518d1b501d6c3c5b218f427f6ff08"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c1b90407ced992331dd6d4f1355819ea1c274cc1ee4d5b7046c6761f9ec11829"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a2fd04ae4971b914e54fe459dd7edbbd3f2ba875d69e057d5e3c8e8cac094935"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-win32.whl", hash = "sha256:b2f317d1678002eee6fe85670039fb34a757972284614638f82b903a03feacdc"},
|
||||
{file = "aiohttp-3.11.18-cp39-cp39-win_amd64.whl", hash = "sha256:5e7007b8d1d09bce37b54111f593d173691c530b80f27c6493b928dabed9e6ef"},
|
||||
{file = "aiohttp-3.11.18.tar.gz", hash = "sha256:ae856e1138612b7e412db63b7708735cff4d38d0399f6a5435d3dac2669f558a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -133,6 +133,23 @@ aiohttp = ">=3.8.1,<4.0.0"
|
||||
async-timeout = ">=4.0.2,<5.0.0"
|
||||
yarl = ">=1.5.1,<2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp-socks"
|
||||
version = "0.10.1"
|
||||
description = "Proxy connector for aiohttp"
|
||||
optional = true
|
||||
python-versions = ">=3.8.0"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"socks\""
|
||||
files = [
|
||||
{file = "aiohttp_socks-0.10.1-py3-none-any.whl", hash = "sha256:6fd4d46c09f952f971a011ff446170daab8d539cf5310c0627f8423df2fb15ea"},
|
||||
{file = "aiohttp_socks-0.10.1.tar.gz", hash = "sha256:49f2e1f8051f2885719beb1b77e312b5a27c3e4b60f0b045a388f194d995e068"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.10.0"
|
||||
python-socks = {version = ">=2.4.3,<3.0.0", extras = ["asyncio"]}
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.3.2"
|
||||
@ -284,6 +301,18 @@ files = [
|
||||
{file = "bidict-0.23.1.tar.gz", hash = "sha256:03069d763bc387bbd20e7d49914e75fc4132a41937fa3405417e1a5a2d006d71"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bracex"
|
||||
version = "2.5.post1"
|
||||
description = "Bash style brace expander."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs"]
|
||||
files = [
|
||||
{file = "bracex-2.5.post1-py3-none-any.whl", hash = "sha256:13e5732fec27828d6af308628285ad358047cec36801598368cb28bc631dbaf6"},
|
||||
{file = "bracex-2.5.post1.tar.gz", hash = "sha256:12c50952415bfa773d2d9ccb8e79651b8cdb1f31a42f6091b804f6ba2b4a66b6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.1.31"
|
||||
@ -400,14 +429,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
version = "8.2.0"
|
||||
description = "Composable command line interface toolkit"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main", "docs"]
|
||||
files = [
|
||||
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
|
||||
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
|
||||
{file = "click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c"},
|
||||
{file = "click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -620,7 +649,7 @@ description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
markers = "python_version == \"3.10\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
@ -1110,46 +1139,46 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.1"
|
||||
version = "3.10.3"
|
||||
description = "Python plotting package"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044"},
|
||||
{file = "matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7"},
|
||||
{file = "matplotlib-3.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea"},
|
||||
{file = "matplotlib-3.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f"},
|
||||
{file = "matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9"},
|
||||
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc"},
|
||||
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4"},
|
||||
{file = "matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779"},
|
||||
{file = "matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba"},
|
||||
{file = "matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7"},
|
||||
{file = "matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb"},
|
||||
{file = "matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb"},
|
||||
{file = "matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30"},
|
||||
{file = "matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8"},
|
||||
{file = "matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd"},
|
||||
{file = "matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8"},
|
||||
{file = "matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d"},
|
||||
{file = "matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049"},
|
||||
{file = "matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b"},
|
||||
{file = "matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220"},
|
||||
{file = "matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1"},
|
||||
{file = "matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea"},
|
||||
{file = "matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4"},
|
||||
{file = "matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee"},
|
||||
{file = "matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a"},
|
||||
{file = "matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7"},
|
||||
{file = "matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d"},
|
||||
{file = "matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5"},
|
||||
{file = "matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4"},
|
||||
{file = "matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751"},
|
||||
{file = "matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014"},
|
||||
{file = "matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1245,30 +1274,33 @@ pyyaml = ">=5.1"
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-include-markdown-plugin"
|
||||
version = "3.9.1"
|
||||
version = "7.1.5"
|
||||
description = "Mkdocs Markdown includer plugin."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["docs"]
|
||||
files = [
|
||||
{file = "mkdocs_include_markdown_plugin-3.9.1-py3-none-any.whl", hash = "sha256:f33687e29ac66d045ba181ea50f054646b0090b42b0a4318f08e7f1d1235e6f6"},
|
||||
{file = "mkdocs_include_markdown_plugin-3.9.1.tar.gz", hash = "sha256:5e5698e78d7fea111be9873a456089daa333497988405acaac8eba2924a19152"},
|
||||
{file = "mkdocs_include_markdown_plugin-7.1.5-py3-none-any.whl", hash = "sha256:d0b96edee45e7fda5eb189e63331cfaf1bf1fbdbebbd08371f1daa77045d3ae9"},
|
||||
{file = "mkdocs_include_markdown_plugin-7.1.5.tar.gz", hash = "sha256:a986967594da6789226798e3c41c70bc17130fadb92b4313f42bd3defdac0adc"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
mkdocs = ">=1.4"
|
||||
wcmatch = "*"
|
||||
|
||||
[package.extras]
|
||||
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)"]
|
||||
cache = ["platformdirs"]
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-material"
|
||||
version = "9.6.11"
|
||||
version = "9.6.14"
|
||||
description = "Documentation that simply works"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs"]
|
||||
files = [
|
||||
{file = "mkdocs_material-9.6.11-py3-none-any.whl", hash = "sha256:47f21ef9cbf4f0ebdce78a2ceecaa5d413581a55141e4464902224ebbc0b1263"},
|
||||
{file = "mkdocs_material-9.6.11.tar.gz", hash = "sha256:0b7f4a0145c5074cdd692e4362d232fb25ef5b23328d0ec1ab287af77cc0deff"},
|
||||
{file = "mkdocs_material-9.6.14-py3-none-any.whl", hash = "sha256:3b9cee6d3688551bf7a8e8f41afda97a3c39a12f0325436d76c86706114b721b"},
|
||||
{file = "mkdocs_material-9.6.14.tar.gz", hash = "sha256:39d795e90dce6b531387c255bd07e866e027828b7346d3eba5ac3de265053754"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -1303,14 +1335,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "mkdocs-swagger-ui-tag"
|
||||
version = "0.7.0"
|
||||
version = "0.7.1"
|
||||
description = "A MkDocs plugin supports for add Swagger UI in page."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs"]
|
||||
files = [
|
||||
{file = "mkdocs_swagger_ui_tag-0.7.0-py3-none-any.whl", hash = "sha256:9ebad56e41da458bd56f5e7a1d428407776f73b3d307649772d9080f56d3036b"},
|
||||
{file = "mkdocs_swagger_ui_tag-0.7.0.tar.gz", hash = "sha256:c218d9be2303fee67eaa2da196f39b2126ac1b660f1692fd978b39b9d1eadb36"},
|
||||
{file = "mkdocs_swagger_ui_tag-0.7.1-py3-none-any.whl", hash = "sha256:e4a1019c96ef333ec4dab0ef7d80068a345c7526a87fe8718f18852ee5ad34a5"},
|
||||
{file = "mkdocs_swagger_ui_tag-0.7.1.tar.gz", hash = "sha256:aed3c5f15297d74241f38cfba4763a5789bf10a410e005014763c66e79576b65"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2071,14 +2103,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
|
||||
|
||||
[[package]]
|
||||
name = "pymdown-extensions"
|
||||
version = "10.14.3"
|
||||
version = "10.15"
|
||||
description = "Extension pack for Python Markdown."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs"]
|
||||
files = [
|
||||
{file = "pymdown_extensions-10.14.3-py3-none-any.whl", hash = "sha256:05e0bee73d64b9c71a4ae17c72abc2f700e8bc8403755a00580b49a4e9f189e9"},
|
||||
{file = "pymdown_extensions-10.14.3.tar.gz", hash = "sha256:41e576ce3f5d650be59e900e4ceff231e0aed2a88cf30acaee41e02f063a061b"},
|
||||
{file = "pymdown_extensions-10.15-py3-none-any.whl", hash = "sha256:46e99bb272612b0de3b7e7caf6da8dd5f4ca5212c0b273feb9304e236c484e5f"},
|
||||
{file = "pymdown_extensions-10.15.tar.gz", hash = "sha256:0e5994e32155f4b03504f939e501b981d306daf7ec2aa1cd2eb6bd300784f8f7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2105,14 +2137,14 @@ diagrams = ["jinja2", "railroad-diagrams"]
|
||||
|
||||
[[package]]
|
||||
name = "pyright"
|
||||
version = "1.1.399"
|
||||
version = "1.1.400"
|
||||
description = "Command line wrapper for pyright"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyright-1.1.399-py3-none-any.whl", hash = "sha256:55f9a875ddf23c9698f24208c764465ffdfd38be6265f7faf9a176e1dc549f3b"},
|
||||
{file = "pyright-1.1.399.tar.gz", hash = "sha256:439035d707a36c3d1b443aec980bc37053fbda88158eded24b8eedcf1c7b7a1b"},
|
||||
{file = "pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e"},
|
||||
{file = "pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2126,14 +2158,14 @@ nodejs = ["nodejs-wheel-binaries"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.4"
|
||||
version = "8.3.5"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
|
||||
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2141,49 +2173,49 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "0.23.8"
|
||||
version = "0.26.0"
|
||||
description = "Pytest support for asyncio"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"},
|
||||
{file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"},
|
||||
{file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"},
|
||||
{file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=7.0.0,<9"
|
||||
pytest = ">=8.2,<9"
|
||||
|
||||
[package.extras]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
|
||||
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
|
||||
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "4.1.0"
|
||||
version = "6.1.1"
|
||||
description = "Pytest plugin for measuring coverage."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
|
||||
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
|
||||
{file = "pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde"},
|
||||
{file = "pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
coverage = {version = ">=5.2.1", extras = ["toml"]}
|
||||
coverage = {version = ">=7.5", extras = ["toml"]}
|
||||
pytest = ">=4.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
|
||||
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-mock"
|
||||
@ -2255,14 +2287,14 @@ docs = ["sphinx"]
|
||||
|
||||
[[package]]
|
||||
name = "python-socketio"
|
||||
version = "5.12.1"
|
||||
version = "5.13.0"
|
||||
description = "Socket.IO server and client for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "python_socketio-5.12.1-py3-none-any.whl", hash = "sha256:24a0ea7cfff0e021eb28c68edbf7914ee4111bdf030b95e4d250c4dc9af7a386"},
|
||||
{file = "python_socketio-5.12.1.tar.gz", hash = "sha256:0299ff1f470b676c09c1bfab1dead25405077d227b2c13cf217a34dadc68ba9c"},
|
||||
{file = "python_socketio-5.13.0-py3-none-any.whl", hash = "sha256:51f68d6499f2df8524668c24bcec13ba1414117cfb3a90115c559b601ab10caf"},
|
||||
{file = "python_socketio-5.13.0.tar.gz", hash = "sha256:ac4e19a0302ae812e23b712ec8b6427ca0521f7c582d6abb096e36e24a263029"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@ -2274,6 +2306,28 @@ asyncio-client = ["aiohttp (>=3.4)"]
|
||||
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[[package]]
|
||||
name = "python-socks"
|
||||
version = "2.7.1"
|
||||
description = "Proxy (SOCKS4, SOCKS5, HTTP CONNECT) client for Python"
|
||||
optional = true
|
||||
python-versions = ">=3.8.0"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"socks\""
|
||||
files = [
|
||||
{file = "python_socks-2.7.1-py3-none-any.whl", hash = "sha256:2603c6454eeaeb82b464ad705be188989e8cf1a4a16f0af3c921d6dd71a49cec"},
|
||||
{file = "python_socks-2.7.1.tar.gz", hash = "sha256:f1a0bb603830fe81e332442eada96757b8f8dec02bd22d1d6f5c99a79704c550"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
async-timeout = {version = ">=4.0", optional = true, markers = "python_version < \"3.11\" and extra == \"asyncio\""}
|
||||
|
||||
[package.extras]
|
||||
anyio = ["anyio (>=3.3.4,<5.0.0)"]
|
||||
asyncio = ["async-timeout (>=4.0) ; python_version < \"3.11\""]
|
||||
curio = ["curio (>=1.4)"]
|
||||
trio = ["trio (>=0.24)"]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
@ -2376,30 +2430,30 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.7"
|
||||
version = "0.11.10"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
|
||||
{file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
|
||||
{file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"},
|
||||
{file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"},
|
||||
{file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"},
|
||||
{file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"},
|
||||
{file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"},
|
||||
{file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"},
|
||||
{file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"},
|
||||
{file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"},
|
||||
{file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"},
|
||||
{file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
|
||||
{file = "ruff-0.11.10-py3-none-linux_armv6l.whl", hash = "sha256:859a7bfa7bc8888abbea31ef8a2b411714e6a80f0d173c2a82f9041ed6b50f58"},
|
||||
{file = "ruff-0.11.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:968220a57e09ea5e4fd48ed1c646419961a0570727c7e069842edd018ee8afed"},
|
||||
{file = "ruff-0.11.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1067245bad978e7aa7b22f67113ecc6eb241dca0d9b696144256c3a879663bca"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4854fd09c7aed5b1590e996a81aeff0c9ff51378b084eb5a0b9cd9518e6cff2"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b4564e9f99168c0f9195a0fd5fa5928004b33b377137f978055e40008a082c5"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b6a9cc5b62c03cc1fea0044ed8576379dbaf751d5503d718c973d5418483641"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:607ecbb6f03e44c9e0a93aedacb17b4eb4f3563d00e8b474298a201622677947"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b3a522fa389402cd2137df9ddefe848f727250535c70dafa840badffb56b7a4"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f071b0deed7e9245d5820dac235cbdd4ef99d7b12ff04c330a241ad3534319f"},
|
||||
{file = "ruff-0.11.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a60e3a0a617eafba1f2e4186d827759d65348fa53708ca547e384db28406a0b"},
|
||||
{file = "ruff-0.11.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:da8ec977eaa4b7bf75470fb575bea2cb41a0e07c7ea9d5a0a97d13dbca697bf2"},
|
||||
{file = "ruff-0.11.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ddf8967e08227d1bd95cc0851ef80d2ad9c7c0c5aab1eba31db49cf0a7b99523"},
|
||||
{file = "ruff-0.11.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5a94acf798a82db188f6f36575d80609072b032105d114b0f98661e1679c9125"},
|
||||
{file = "ruff-0.11.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3afead355f1d16d95630df28d4ba17fb2cb9c8dfac8d21ced14984121f639bad"},
|
||||
{file = "ruff-0.11.10-py3-none-win32.whl", hash = "sha256:dc061a98d32a97211af7e7f3fa1d4ca2fcf919fb96c28f39551f35fc55bdbc19"},
|
||||
{file = "ruff-0.11.10-py3-none-win_amd64.whl", hash = "sha256:5cc725fbb4d25b0f185cb42df07ab6b76c4489b4bfb740a175f3a59c70e8a224"},
|
||||
{file = "ruff-0.11.10-py3-none-win_arm64.whl", hash = "sha256:ef69637b35fb8b210743926778d0e45e1bffa850a7c61e428c6b971549b5f5d1"},
|
||||
{file = "ruff-0.11.10.tar.gz", hash = "sha256:d522fb204b4959909ecac47da02830daec102eeb100fb50ea9554818d47a5fa6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2612,6 +2666,21 @@ files = [
|
||||
[package.extras]
|
||||
watchmedo = ["PyYAML (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "wcmatch"
|
||||
version = "10.0"
|
||||
description = "Wildcard/glob file name matcher."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["docs"]
|
||||
files = [
|
||||
{file = "wcmatch-10.0-py3-none-any.whl", hash = "sha256:0dd927072d03c0a6527a20d2e6ad5ba8d0380e60870c383bc533b71744df7b7a"},
|
||||
{file = "wcmatch-10.0.tar.gz", hash = "sha256:e72f0de09bba6a04e0de70937b0cf06e55f36f37b3deb422dfaf854b867b840a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
bracex = ">=2.1.1"
|
||||
|
||||
[[package]]
|
||||
name = "websocket-client"
|
||||
version = "1.8.0"
|
||||
@ -2746,7 +2815,10 @@ idna = ">=2.0"
|
||||
multidict = ">=4.0"
|
||||
propcache = ">=0.2.1"
|
||||
|
||||
[extras]
|
||||
socks = ["aiohttp-socks"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "9c3e1eddb7fccc81a8cb3578d67658cf03189f283a743c4ea258d4d50d2486b1"
|
||||
python-versions = ">=3.10,<4.0"
|
||||
content-hash = "07754bc1fa6fc5e4b15c253a68cfe32368ae0a1bb9e83d8f7fd80ee61013c401"
|
||||
|
@ -1,50 +1,56 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "pydase"
|
||||
version = "0.10.11"
|
||||
version = "0.10.15"
|
||||
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 = [
|
||||
{name = "Mose Müller",email = "mosemueller@gmail.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
packages = [{ include = "pydase", from = "src" }]
|
||||
requires-python = ">=3.10,<4.0"
|
||||
dependencies = [
|
||||
"toml (>=0.10.2,<0.11.0)",
|
||||
"python-socketio (>=5.13.0,<6.0.0)",
|
||||
"confz (>=2.1.0,<3.0.0)",
|
||||
"pint (>=0.24.4,<0.25.0)",
|
||||
"websocket-client (>=1.8.0,<2.0.0)",
|
||||
"aiohttp (>=3.11.18,<4.0.0)",
|
||||
"click (>=8.2.0,<9.0.0)",
|
||||
"aiohttp-middlewares (>=2.4.0,<3.0.0)",
|
||||
"anyio (>=4.9.0,<5.0.0)"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
socks = ["aiohttp-socks (>=0.10.1,<0.11.0)"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
toml = "^0.10.2"
|
||||
python-socketio = "^5.8.0"
|
||||
confz = "^2.0.0"
|
||||
pint = "^0.24"
|
||||
websocket-client = "^1.7.0"
|
||||
aiohttp = "^3.9.3"
|
||||
click = "^8.1.7"
|
||||
aiohttp-middlewares = "^2.3.0"
|
||||
anyio = "^4.6.0"
|
||||
[tool.poetry]
|
||||
packages = [{include = "pydase", from = "src"}]
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
types-toml = "^0.10.8.6"
|
||||
pytest = "^7.4.0"
|
||||
pytest-cov = "^4.1.0"
|
||||
mypy = "^1.4.1"
|
||||
matplotlib = "^3.7.2"
|
||||
pyright = "^1.1.323"
|
||||
pytest-mock = "^3.11.1"
|
||||
ruff = "^0.5.0"
|
||||
pytest-asyncio = "^0.23.2"
|
||||
types-toml = "^0.10.8.20240310"
|
||||
pytest = "^8.3.5"
|
||||
pytest-cov = "^6.1.1"
|
||||
mypy = "^1.15.0"
|
||||
matplotlib = "^3.10.3"
|
||||
pyright = "^1.1.400"
|
||||
pytest-mock = "^3.14.0"
|
||||
ruff = "^0.11.10"
|
||||
pytest-asyncio = "^0.26.0"
|
||||
|
||||
[tool.poetry.group.docs]
|
||||
optional = true
|
||||
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.5.30"
|
||||
mkdocs-include-markdown-plugin = "^3.9.1"
|
||||
mkdocstrings = {extras = ["python"], version = "^0.29.0"}
|
||||
pymdown-extensions = "^10.1"
|
||||
mkdocs-swagger-ui-tag = "^0.7.0"
|
||||
mkdocs-material = "^9.6.14"
|
||||
mkdocs-include-markdown-plugin = "^7.1.5"
|
||||
mkdocstrings = {extras = ["python"], version = "^0.29.1"}
|
||||
pymdown-extensions = "^10.15"
|
||||
mkdocs-swagger-ui-tag = "^0.7.1"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
@ -86,6 +92,7 @@ select = [
|
||||
ignore = [
|
||||
"RUF006", # asyncio-dangling-task
|
||||
"PERF203", # try-except-in-loop
|
||||
"ASYNC110", # async-busy-wait
|
||||
]
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
@ -104,3 +111,10 @@ disallow_incomplete_defs = true
|
||||
disallow_any_generics = true
|
||||
check_untyped_defs = true
|
||||
ignore_missing_imports = false
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
filterwarnings = [
|
||||
# I don't controll the usage of the timeout
|
||||
"ignore:parameter 'timeout' of type 'float' is deprecated, please use 'timeout=ClientWSTimeout"
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ from pydase.utils.logging import setup_logging
|
||||
setup_logging()
|
||||
|
||||
__all__ = [
|
||||
"Client",
|
||||
"DataService",
|
||||
"Server",
|
||||
"Client",
|
||||
]
|
||||
|
@ -1,11 +1,14 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
import sys
|
||||
import threading
|
||||
import urllib.parse
|
||||
from builtins import ModuleNotFoundError
|
||||
from types import TracebackType
|
||||
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
||||
|
||||
import aiohttp
|
||||
import socketio # type: ignore
|
||||
|
||||
from pydase.client.proxy_class import ProxyClass
|
||||
@ -33,51 +36,60 @@ class NotifyDict(TypedDict):
|
||||
|
||||
def asyncio_loop_thread(loop: asyncio.AbstractEventLoop) -> None:
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
try:
|
||||
loop.run_forever()
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
class Client:
|
||||
"""
|
||||
A client for connecting to a remote pydase service using socket.io. This client
|
||||
"""A client for connecting to a remote pydase service using Socket.IO. This client
|
||||
handles asynchronous communication with a service, manages events such as
|
||||
connection, disconnection, and updates, and ensures that the proxy object is
|
||||
up-to-date with the server state.
|
||||
|
||||
Args:
|
||||
url:
|
||||
The URL of the pydase Socket.IO server. This should always contain the
|
||||
protocol and the hostname.
|
||||
block_until_connected:
|
||||
If set to True, the constructor will block until the connection to the
|
||||
service has been established. This is useful for ensuring the client is
|
||||
ready to use immediately after instantiation. Default is True.
|
||||
sio_client_kwargs:
|
||||
Additional keyword arguments passed to the underlying
|
||||
url: The URL of the pydase Socket.IO server. This should always contain the
|
||||
protocol (e.g., `ws` or `wss`) and the hostname, and can optionally include
|
||||
a path prefix (e.g., `ws://localhost:8001/service`).
|
||||
block_until_connected: If set to True, the constructor will block until the
|
||||
connection to the service has been established. This is useful for ensuring
|
||||
the client is ready to use immediately after instantiation. Default is True.
|
||||
sio_client_kwargs: Additional keyword arguments passed to the underlying
|
||||
[`AsyncClient`][socketio.AsyncClient]. This allows fine-tuning of the
|
||||
client's behaviour (e.g., reconnection attempts or reconnection delay).
|
||||
Default is an empty dictionary.
|
||||
client_id: Client identification that will be shown in the server logs this
|
||||
client is connecting to. This ID is passed as a `X-Client-Id` header in the
|
||||
HTTP(s) request. Defaults to None.
|
||||
client_id: An optional client identifier. This ID is sent to the server as the
|
||||
`X-Client-Id` HTTP header. It can be used for logging or authentication
|
||||
purposes on the server side. If not provided, it defaults to the hostname
|
||||
of the machine running the client.
|
||||
proxy_url: An optional proxy URL to route the connection through. This is useful
|
||||
if the service is only reachable via an SSH tunnel or behind a firewall
|
||||
(e.g., `socks5://localhost:2222`).
|
||||
|
||||
Example:
|
||||
The following example demonstrates a `Client` instance that connects to another
|
||||
pydase service, while customising some of the connection settings for the
|
||||
underlying [`AsyncClient`][socketio.AsyncClient].
|
||||
Connect to a service directly:
|
||||
|
||||
```python
|
||||
pydase.Client(url="ws://localhost:8001", sio_client_kwargs={
|
||||
"reconnection_attempts": 2,
|
||||
"reconnection_delay": 2,
|
||||
"reconnection_delay_max": 8,
|
||||
})
|
||||
client = pydase.Client(url="ws://localhost:8001")
|
||||
```
|
||||
|
||||
When connecting to a server over a secure connection (i.e., the server is using
|
||||
SSL/TLS encryption), make sure that the `wss` protocol is used instead of `ws`:
|
||||
Connect over a secure connection:
|
||||
|
||||
```python
|
||||
pydase.Client(url="wss://my-service.example.com")
|
||||
client = pydase.Client(url="wss://my-service.example.com")
|
||||
```
|
||||
|
||||
Connect using a SOCKS5 proxy (e.g., through an SSH tunnel):
|
||||
|
||||
```bash
|
||||
ssh -D 2222 user@gateway.example.com
|
||||
```
|
||||
|
||||
```python
|
||||
client = pydase.Client(
|
||||
url="ws://remote-server:8001",
|
||||
proxy_url="socks5://localhost:2222"
|
||||
)
|
||||
```
|
||||
"""
|
||||
|
||||
@ -88,6 +100,7 @@ class Client:
|
||||
block_until_connected: bool = True,
|
||||
sio_client_kwargs: dict[str, Any] = {},
|
||||
client_id: str | None = None,
|
||||
proxy_url: str | None = None,
|
||||
):
|
||||
# Parse the URL to separate base URL and path prefix
|
||||
parsed_url = urllib.parse.urlparse(url)
|
||||
@ -100,18 +113,14 @@ class Client:
|
||||
# Store the path prefix (e.g., "/service" in "ws://localhost:8081/service")
|
||||
self._path_prefix = parsed_url.path.rstrip("/") # Remove trailing slash if any
|
||||
self._url = url
|
||||
self._sio = socketio.AsyncClient(**sio_client_kwargs)
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._client_id = client_id
|
||||
self.proxy = ProxyClass(
|
||||
sio_client=self._sio, loop=self._loop, reconnect=self.connect
|
||||
)
|
||||
self._proxy_url = proxy_url
|
||||
self._client_id = client_id or socket.gethostname()
|
||||
self._sio_client_kwargs = sio_client_kwargs
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self.proxy: ProxyClass
|
||||
"""A proxy object representing the remote service, facilitating interaction as
|
||||
if it were local."""
|
||||
self._thread = threading.Thread(
|
||||
target=asyncio_loop_thread, args=(self._loop,), daemon=True
|
||||
)
|
||||
self._thread.start()
|
||||
self.connect(block_until_connected=block_until_connected)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
@ -126,17 +135,72 @@ class Client:
|
||||
self.disconnect()
|
||||
|
||||
def connect(self, block_until_connected: bool = True) -> None:
|
||||
if self._thread is None or self._loop is None:
|
||||
self._loop = self._initialize_loop_and_thread()
|
||||
self._initialize_socketio_client()
|
||||
self.proxy = ProxyClass(
|
||||
sio_client=self._sio,
|
||||
loop=self._loop,
|
||||
reconnect=self.connect,
|
||||
)
|
||||
|
||||
connection_future = asyncio.run_coroutine_threadsafe(
|
||||
self._connect(), self._loop
|
||||
)
|
||||
if block_until_connected:
|
||||
connection_future.result()
|
||||
|
||||
def disconnect(self) -> None:
|
||||
connection_future = asyncio.run_coroutine_threadsafe(
|
||||
self._disconnect(), self._loop
|
||||
def _initialize_socketio_client(self) -> None:
|
||||
if self._proxy_url is not None:
|
||||
try:
|
||||
import aiohttp_socks.connector
|
||||
except ModuleNotFoundError:
|
||||
raise ModuleNotFoundError(
|
||||
"Missing dependency 'aiohttp_socks'. To use SOCKS5 proxy support, "
|
||||
"install the optional 'socks' extra:\n\n"
|
||||
' pip install "pydase[socks]"\n\n'
|
||||
"This is required when specifying a `proxy_url` for "
|
||||
"`pydase.Client`."
|
||||
)
|
||||
|
||||
session = aiohttp.ClientSession(
|
||||
connector=aiohttp_socks.connector.ProxyConnector.from_url(
|
||||
url=self._proxy_url, loop=self._loop
|
||||
),
|
||||
loop=self._loop,
|
||||
)
|
||||
self._sio = socketio.AsyncClient(
|
||||
http_session=session, **self._sio_client_kwargs
|
||||
)
|
||||
else:
|
||||
self._sio = socketio.AsyncClient(**self._sio_client_kwargs)
|
||||
|
||||
def _initialize_loop_and_thread(self) -> asyncio.AbstractEventLoop:
|
||||
"""Initialize a new asyncio event loop, start it in a background thread,
|
||||
and create the ProxyClass instance bound to that loop.
|
||||
"""
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
self._thread = threading.Thread(
|
||||
target=asyncio_loop_thread,
|
||||
args=(loop,),
|
||||
daemon=True,
|
||||
)
|
||||
connection_future.result()
|
||||
self._thread.start()
|
||||
|
||||
return loop
|
||||
|
||||
def disconnect(self) -> None:
|
||||
if self._loop is not None and self._thread is not None:
|
||||
connection_future = asyncio.run_coroutine_threadsafe(
|
||||
self._disconnect(), self._loop
|
||||
)
|
||||
connection_future.result()
|
||||
|
||||
# Stop the event loop and thread
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
self._thread.join()
|
||||
self._thread = None
|
||||
|
||||
async def _connect(self) -> None:
|
||||
logger.debug("Connecting to server '%s' ...", self._url)
|
||||
@ -165,7 +229,7 @@ class Client:
|
||||
async def _handle_connect(self) -> None:
|
||||
logger.debug("Connected to '%s' ...", self._url)
|
||||
serialized_object = cast(
|
||||
SerializedDataService, await self._sio.call("service_serialization")
|
||||
"SerializedDataService", await self._sio.call("service_serialization")
|
||||
)
|
||||
ProxyLoader.update_data_service_proxy(
|
||||
self.proxy, serialized_object=serialized_object
|
||||
|
@ -67,7 +67,7 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
|
||||
def serialize(self) -> SerializedObject:
|
||||
if self._service_representation is None:
|
||||
serialization_future = cast(
|
||||
asyncio.Future[SerializedDataService],
|
||||
"asyncio.Future[SerializedDataService]",
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self._sio.call("service_serialization"), self._loop
|
||||
),
|
||||
@ -80,7 +80,7 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
|
||||
self._service_representation = serialization_future.result()
|
||||
|
||||
device_connection_value = cast(
|
||||
dict[str, SerializedObject],
|
||||
"dict[str, SerializedObject]",
|
||||
pydase.components.DeviceConnection().serialize()["value"],
|
||||
)
|
||||
|
||||
@ -90,7 +90,7 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection):
|
||||
|
||||
value = {
|
||||
**cast(
|
||||
dict[str, SerializedObject],
|
||||
"dict[str, SerializedObject]",
|
||||
# need to deepcopy to not overwrite the _service_representation dict
|
||||
# when adding a prefix with add_prefix_to_full_access_path
|
||||
deepcopy(self._service_representation["value"]),
|
||||
|
@ -123,35 +123,35 @@ class ProxyList(list[Any]):
|
||||
|
||||
update_value(self._sio, self._loop, full_access_path, value)
|
||||
|
||||
def append(self, __object: Any) -> None:
|
||||
def append(self, object_: Any, /) -> None:
|
||||
full_access_path = f"{self._parent_path}.append"
|
||||
|
||||
trigger_method(self._sio, self._loop, full_access_path, [__object], {})
|
||||
trigger_method(self._sio, self._loop, full_access_path, [object_], {})
|
||||
|
||||
def clear(self) -> None:
|
||||
full_access_path = f"{self._parent_path}.clear"
|
||||
|
||||
trigger_method(self._sio, self._loop, full_access_path, [], {})
|
||||
|
||||
def extend(self, __iterable: Iterable[Any]) -> None:
|
||||
def extend(self, iterable: Iterable[Any], /) -> None:
|
||||
full_access_path = f"{self._parent_path}.extend"
|
||||
|
||||
trigger_method(self._sio, self._loop, full_access_path, [__iterable], {})
|
||||
trigger_method(self._sio, self._loop, full_access_path, [iterable], {})
|
||||
|
||||
def insert(self, __index: SupportsIndex, __object: Any) -> None:
|
||||
def insert(self, index: SupportsIndex, object_: Any, /) -> None:
|
||||
full_access_path = f"{self._parent_path}.insert"
|
||||
|
||||
trigger_method(self._sio, self._loop, full_access_path, [__index, __object], {})
|
||||
trigger_method(self._sio, self._loop, full_access_path, [index, object_], {})
|
||||
|
||||
def pop(self, __index: SupportsIndex = -1) -> Any:
|
||||
def pop(self, index: SupportsIndex = -1, /) -> Any:
|
||||
full_access_path = f"{self._parent_path}.pop"
|
||||
|
||||
return trigger_method(self._sio, self._loop, full_access_path, [__index], {})
|
||||
return trigger_method(self._sio, self._loop, full_access_path, [index], {})
|
||||
|
||||
def remove(self, __value: Any) -> None:
|
||||
def remove(self, value: Any, /) -> None:
|
||||
full_access_path = f"{self._parent_path}.remove"
|
||||
|
||||
trigger_method(self._sio, self._loop, full_access_path, [__value], {})
|
||||
trigger_method(self._sio, self._loop, full_access_path, [value], {})
|
||||
|
||||
|
||||
class ProxyClassMixin:
|
||||
@ -266,7 +266,7 @@ class ProxyLoader:
|
||||
return ProxyList(
|
||||
[
|
||||
ProxyLoader.loads_proxy(item, sio_client, loop)
|
||||
for item in cast(list[SerializedObject], serialized_object["value"])
|
||||
for item in cast("list[SerializedObject]", serialized_object["value"])
|
||||
],
|
||||
parent_path=serialized_object["full_access_path"],
|
||||
sio_client=sio_client,
|
||||
@ -283,7 +283,7 @@ class ProxyLoader:
|
||||
{
|
||||
key: ProxyLoader.loads_proxy(value, sio_client, loop)
|
||||
for key, value in cast(
|
||||
dict[str, SerializedObject], serialized_object["value"]
|
||||
"dict[str, SerializedObject]", serialized_object["value"]
|
||||
).items()
|
||||
},
|
||||
parent_path=serialized_object["full_access_path"],
|
||||
@ -300,7 +300,7 @@ class ProxyLoader:
|
||||
proxy_class._proxy_setters.clear()
|
||||
proxy_class._proxy_methods.clear()
|
||||
for key, value in cast(
|
||||
dict[str, SerializedObject], serialized_object["value"]
|
||||
"dict[str, SerializedObject]", serialized_object["value"]
|
||||
).items():
|
||||
type_handler: dict[str | None, None | Callable[..., Any]] = {
|
||||
None: None,
|
||||
@ -333,7 +333,7 @@ class ProxyLoader:
|
||||
) -> Any:
|
||||
# Custom types like Components or DataService classes
|
||||
component_class = cast(
|
||||
type, Deserializer.get_service_base_class(serialized_object["type"])
|
||||
"type", Deserializer.get_service_base_class(serialized_object["type"])
|
||||
)
|
||||
class_bases = (
|
||||
ProxyClassMixin,
|
||||
|
@ -33,8 +33,8 @@ from pydase.components.image import Image
|
||||
from pydase.components.number_slider import NumberSlider
|
||||
|
||||
__all__ = [
|
||||
"NumberSlider",
|
||||
"Image",
|
||||
"ColouredEnum",
|
||||
"DeviceConnection",
|
||||
"Image",
|
||||
"NumberSlider",
|
||||
]
|
||||
|
@ -15,9 +15,9 @@ from pydase.utils.helpers import (
|
||||
is_property_attribute,
|
||||
)
|
||||
from pydase.utils.serialization.serializer import (
|
||||
SerializedObject,
|
||||
Serializer,
|
||||
)
|
||||
from pydase.utils.serialization.types import SerializedObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -27,17 +27,17 @@ class DataService(AbstractDataService):
|
||||
super().__init__()
|
||||
self.__check_instance_classes()
|
||||
|
||||
def __setattr__(self, __name: str, __value: Any) -> None:
|
||||
def __setattr__(self, name: str, value: Any, /) -> None:
|
||||
# Check and warn for unexpected type changes in attributes
|
||||
self._warn_on_type_change(__name, __value)
|
||||
self._warn_on_type_change(name, value)
|
||||
|
||||
# every class defined by the user should inherit from DataService if it is
|
||||
# assigned to a public attribute
|
||||
if not __name.startswith("_") and not inspect.isfunction(__value):
|
||||
self.__warn_if_not_observable(__value)
|
||||
if not name.startswith("_") and not inspect.isfunction(value):
|
||||
self.__warn_if_not_observable(value)
|
||||
|
||||
# Set the attribute
|
||||
super().__setattr__(__name, __value)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
def _warn_on_type_change(self, attr_name: str, new_value: Any) -> None:
|
||||
if is_property_attribute(self, attr_name):
|
||||
@ -56,16 +56,14 @@ class DataService(AbstractDataService):
|
||||
|
||||
def _is_unexpected_type_change(self, current_value: Any, new_value: Any) -> bool:
|
||||
return (
|
||||
isinstance(current_value, float)
|
||||
and not isinstance(new_value, float)
|
||||
or (
|
||||
isinstance(current_value, u.Quantity)
|
||||
and not isinstance(new_value, u.Quantity)
|
||||
)
|
||||
isinstance(current_value, float) and not isinstance(new_value, float)
|
||||
) or (
|
||||
isinstance(current_value, u.Quantity)
|
||||
and not isinstance(new_value, u.Quantity)
|
||||
)
|
||||
|
||||
def __warn_if_not_observable(self, __value: Any) -> None:
|
||||
value_class = __value if inspect.isclass(__value) else __value.__class__
|
||||
def __warn_if_not_observable(self, value: Any, /) -> None:
|
||||
value_class = value if inspect.isclass(value) else value.__class__
|
||||
|
||||
if not issubclass(
|
||||
value_class,
|
||||
@ -81,7 +79,7 @@ class DataService(AbstractDataService):
|
||||
| Observable
|
||||
| Callable
|
||||
),
|
||||
) and not is_descriptor(__value):
|
||||
) and not is_descriptor(value):
|
||||
logger.warning(
|
||||
"Class '%s' does not inherit from DataService. This may lead to"
|
||||
" unexpected behaviour!",
|
||||
|
@ -2,10 +2,10 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from pydase.utils.serialization.serializer import (
|
||||
SerializedObject,
|
||||
get_nested_dict_by_path,
|
||||
set_nested_value_by_path,
|
||||
)
|
||||
from pydase.utils.serialization.types import SerializedObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydase import DataService
|
||||
@ -46,13 +46,13 @@ class DataServiceCache:
|
||||
|
||||
def update_cache(self, full_access_path: str, value: Any) -> None:
|
||||
set_nested_value_by_path(
|
||||
cast(dict[str, SerializedObject], self._cache["value"]),
|
||||
cast("dict[str, SerializedObject]", self._cache["value"]),
|
||||
full_access_path,
|
||||
value,
|
||||
)
|
||||
|
||||
def get_value_dict_from_cache(self, full_access_path: str) -> SerializedObject:
|
||||
return get_nested_dict_by_path(
|
||||
cast(dict[str, SerializedObject], self._cache["value"]),
|
||||
cast("dict[str, SerializedObject]", self._cache["value"]),
|
||||
full_access_path,
|
||||
)
|
||||
|
@ -13,13 +13,26 @@ from pydase.utils.helpers import (
|
||||
)
|
||||
from pydase.utils.serialization.serializer import (
|
||||
SerializationPathError,
|
||||
SerializedObject,
|
||||
dump,
|
||||
)
|
||||
from pydase.utils.serialization.types import SerializedObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_nested_attribute(full_access_path: str, changing_attributes: list[str]) -> bool:
|
||||
"""Return True if the full_access_path is a nested attribute of any
|
||||
changing_attribute."""
|
||||
|
||||
return any(
|
||||
(
|
||||
full_access_path.startswith((f"{attr}.", f"{attr}["))
|
||||
and full_access_path != attr
|
||||
)
|
||||
for attr in changing_attributes
|
||||
)
|
||||
|
||||
|
||||
class DataServiceObserver(PropertyObserver):
|
||||
def __init__(self, state_manager: StateManager) -> None:
|
||||
self.state_manager = state_manager
|
||||
@ -29,11 +42,7 @@ class DataServiceObserver(PropertyObserver):
|
||||
super().__init__(state_manager.service)
|
||||
|
||||
def on_change(self, full_access_path: str, value: Any) -> None:
|
||||
if any(
|
||||
full_access_path.startswith(changing_attribute)
|
||||
and full_access_path != changing_attribute
|
||||
for changing_attribute in self.changing_attributes
|
||||
):
|
||||
if _is_nested_attribute(full_access_path, self.changing_attributes):
|
||||
return
|
||||
cached_value_dict: SerializedObject
|
||||
|
||||
|
@ -17,11 +17,11 @@ from pydase.utils.helpers import (
|
||||
from pydase.utils.serialization.deserializer import loads
|
||||
from pydase.utils.serialization.serializer import (
|
||||
SerializationPathError,
|
||||
SerializedObject,
|
||||
generate_serialized_data_paths,
|
||||
get_nested_dict_by_path,
|
||||
serialized_dict_is_nested_object,
|
||||
)
|
||||
from pydase.utils.serialization.types import SerializedObject
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pydase import DataService
|
||||
@ -141,7 +141,7 @@ class StateManager:
|
||||
@property
|
||||
def cache_value(self) -> dict[str, SerializedObject]:
|
||||
"""Returns the "value" value of the DataService serialization."""
|
||||
return cast(dict[str, SerializedObject], self.cache_manager.cache["value"])
|
||||
return cast("dict[str, SerializedObject]", self.cache_manager.cache["value"])
|
||||
|
||||
def save_state(self) -> None:
|
||||
"""Saves the DataService's current state to a JSON file defined by
|
||||
@ -203,7 +203,7 @@ class StateManager:
|
||||
with open(self.filename) as f:
|
||||
# Load JSON data from file and update class attributes with these
|
||||
# values
|
||||
return cast(dict[str, Any], json.load(f))
|
||||
return cast("dict[str, Any]", json.load(f))
|
||||
return {}
|
||||
|
||||
def set_service_attribute_value_by_path(
|
||||
|
File diff suppressed because one or more lines are too long
@ -7,7 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site displaying a pydase UI." />
|
||||
<script type="module" crossorigin src="/assets/index-DpoEqi_N.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BLJetjaQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DJzFvk4W.css">
|
||||
</head>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import weakref
|
||||
from abc import ABC, abstractmethod
|
||||
@ -164,9 +165,9 @@ class _ObservableList(ObservableObject, list[Any]):
|
||||
|
||||
self._notify_changed(f"[{key}]", value)
|
||||
|
||||
def append(self, __object: Any) -> None:
|
||||
def append(self, object_: Any, /) -> None:
|
||||
self._notify_change_start("")
|
||||
super().append(self._initialise_new_objects(f"[{len(self)}]", __object))
|
||||
super().append(self._initialise_new_objects(f"[{len(self)}]", object_))
|
||||
self._notify_changed("", self)
|
||||
|
||||
def clear(self) -> None:
|
||||
@ -176,33 +177,33 @@ class _ObservableList(ObservableObject, list[Any]):
|
||||
|
||||
self._notify_changed("", self)
|
||||
|
||||
def extend(self, __iterable: Iterable[Any]) -> None:
|
||||
def extend(self, iterable: Iterable[Any], /) -> None:
|
||||
self._remove_self_from_observables()
|
||||
|
||||
try:
|
||||
super().extend(__iterable)
|
||||
super().extend(iterable)
|
||||
finally:
|
||||
for i, item in enumerate(self):
|
||||
super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item))
|
||||
|
||||
self._notify_changed("", self)
|
||||
|
||||
def insert(self, __index: SupportsIndex, __object: Any) -> None:
|
||||
def insert(self, index: SupportsIndex, object_: Any, /) -> None:
|
||||
self._remove_self_from_observables()
|
||||
|
||||
try:
|
||||
super().insert(__index, __object)
|
||||
super().insert(index, object_)
|
||||
finally:
|
||||
for i, item in enumerate(self):
|
||||
super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item))
|
||||
|
||||
self._notify_changed("", self)
|
||||
|
||||
def pop(self, __index: SupportsIndex = -1) -> Any:
|
||||
def pop(self, index: SupportsIndex = -1, /) -> Any:
|
||||
self._remove_self_from_observables()
|
||||
|
||||
try:
|
||||
popped_item = super().pop(__index)
|
||||
popped_item = super().pop(index)
|
||||
finally:
|
||||
for i, item in enumerate(self):
|
||||
super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item))
|
||||
@ -210,11 +211,11 @@ class _ObservableList(ObservableObject, list[Any]):
|
||||
self._notify_changed("", self)
|
||||
return popped_item
|
||||
|
||||
def remove(self, __value: Any) -> None:
|
||||
def remove(self, value: Any, /) -> None:
|
||||
self._remove_self_from_observables()
|
||||
|
||||
try:
|
||||
super().remove(__value)
|
||||
super().remove(value)
|
||||
finally:
|
||||
for i, item in enumerate(self):
|
||||
super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item))
|
||||
@ -252,7 +253,8 @@ class _ObservableDict(ObservableObject, dict[str, Any]):
|
||||
self.__setitem__(key, self._initialise_new_objects(f'["{key}"]', value))
|
||||
|
||||
def __del__(self) -> None:
|
||||
self._dict_mapping.pop(id(self._original_dict))
|
||||
with contextlib.suppress(KeyError):
|
||||
self._dict_mapping.pop(id(self._original_dict))
|
||||
|
||||
def __setitem__(self, key: str, value: Any) -> None:
|
||||
if not isinstance(key, str):
|
||||
|
@ -22,7 +22,7 @@ def reverse_dict(original_dict: dict[str, list[str]]) -> dict[str, list[str]]:
|
||||
|
||||
def get_property_dependencies(prop: property, prefix: str = "") -> list[str]:
|
||||
source_code_string = inspect.getsource(prop.fget) # type: ignore[arg-type]
|
||||
pattern = r"self\.([^\s\{\}]+)"
|
||||
pattern = r"self\.([^\s\{\}\(\)]+)"
|
||||
matches = re.findall(pattern, source_code_string)
|
||||
return [prefix + match for match in matches if "(" not in match]
|
||||
|
||||
|
@ -14,7 +14,6 @@ from pydase.data_service.data_service_observer import DataServiceObserver
|
||||
from pydase.data_service.state_manager import StateManager
|
||||
from pydase.server.web_server import WebServer
|
||||
from pydase.task.autostart import autostart_service_tasks
|
||||
from pydase.utils.helpers import current_event_loop_exists
|
||||
|
||||
HANDLED_SIGNALS = (
|
||||
signal.SIGINT, # Unix signal 2. Sent by Ctrl+C.
|
||||
@ -162,6 +161,10 @@ class Server:
|
||||
self._additional_servers = additional_servers
|
||||
self.should_exit = False
|
||||
self.servers: dict[str, asyncio.Future[Any]] = {}
|
||||
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
|
||||
self._state_manager = StateManager(
|
||||
service=self._service,
|
||||
filename=filename,
|
||||
@ -170,11 +173,6 @@ class Server:
|
||||
self._observer = DataServiceObserver(self._state_manager)
|
||||
self._state_manager.load_state()
|
||||
autostart_service_tasks(self._service)
|
||||
if not current_event_loop_exists():
|
||||
self._loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(self._loop)
|
||||
else:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
@ -182,7 +180,10 @@ class Server:
|
||||
|
||||
This method should be called to start the server after it's been instantiated.
|
||||
"""
|
||||
self._loop.run_until_complete(self.serve())
|
||||
try:
|
||||
self._loop.run_until_complete(self.serve())
|
||||
finally:
|
||||
self._loop.close()
|
||||
|
||||
async def serve(self) -> None:
|
||||
process_id = os.getpid()
|
||||
|
@ -20,7 +20,7 @@ from pydase.data_service.data_service_observer import DataServiceObserver
|
||||
from pydase.data_service.state_manager import StateManager
|
||||
from pydase.server.web_server.api.v1 import endpoints
|
||||
from pydase.utils.logging import SocketIOHandler
|
||||
from pydase.utils.serialization.serializer import SerializedObject
|
||||
from pydase.utils.serialization.types import SerializedObject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -219,7 +219,18 @@ def is_descriptor(obj: object) -> bool:
|
||||
|
||||
|
||||
def current_event_loop_exists() -> bool:
|
||||
"""Check if an event loop has been set."""
|
||||
"""Check if a running and open asyncio event loop exists in the current thread.
|
||||
|
||||
This checks if an event loop is set via the current event loop policy and verifies
|
||||
that the loop has not been closed.
|
||||
|
||||
Returns:
|
||||
True if an event loop exists and is not closed, False otherwise.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
|
||||
return asyncio.get_event_loop_policy()._local._loop is not None # type: ignore
|
||||
try:
|
||||
return not asyncio.get_event_loop().is_closed()
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
@ -85,7 +85,7 @@ class Deserializer:
|
||||
def deserialize_list(cls, serialized_object: SerializedObject) -> Any:
|
||||
return [
|
||||
cls.deserialize(item)
|
||||
for item in cast(list[SerializedObject], serialized_object["value"])
|
||||
for item in cast("list[SerializedObject]", serialized_object["value"])
|
||||
]
|
||||
|
||||
@classmethod
|
||||
@ -93,7 +93,7 @@ class Deserializer:
|
||||
return {
|
||||
key: cls.deserialize(value)
|
||||
for key, value in cast(
|
||||
dict[str, SerializedObject], serialized_object["value"]
|
||||
"dict[str, SerializedObject]", serialized_object["value"]
|
||||
).items()
|
||||
}
|
||||
|
||||
@ -148,7 +148,7 @@ class Deserializer:
|
||||
|
||||
# Process and add properties based on the serialized object
|
||||
for key, value in cast(
|
||||
dict[str, SerializedObject], serialized_object["value"]
|
||||
"dict[str, SerializedObject]", serialized_object["value"]
|
||||
).items():
|
||||
if value["type"] != "method":
|
||||
class_attrs[key] = cls.create_attr_property(value)
|
||||
|
@ -20,29 +20,29 @@ from pydase.utils.helpers import (
|
||||
parse_full_access_path,
|
||||
parse_serialized_key,
|
||||
)
|
||||
from pydase.utils.serialization.types import (
|
||||
DataServiceTypes,
|
||||
SerializedBool,
|
||||
SerializedDataService,
|
||||
SerializedDatetime,
|
||||
SerializedDict,
|
||||
SerializedEnum,
|
||||
SerializedException,
|
||||
SerializedFloat,
|
||||
SerializedInteger,
|
||||
SerializedList,
|
||||
SerializedMethod,
|
||||
SerializedNoneType,
|
||||
SerializedObject,
|
||||
SerializedQuantity,
|
||||
SerializedString,
|
||||
SignatureDict,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from pydase.client.proxy_class import ProxyClass
|
||||
from pydase.utils.serialization.types import (
|
||||
DataServiceTypes,
|
||||
SerializedBool,
|
||||
SerializedDataService,
|
||||
SerializedDatetime,
|
||||
SerializedDict,
|
||||
SerializedEnum,
|
||||
SerializedException,
|
||||
SerializedFloat,
|
||||
SerializedInteger,
|
||||
SerializedList,
|
||||
SerializedMethod,
|
||||
SerializedNoneType,
|
||||
SerializedObject,
|
||||
SerializedQuantity,
|
||||
SerializedString,
|
||||
SignatureDict,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -253,7 +253,7 @@ class Serializer:
|
||||
|
||||
for k, v in sig.parameters.items():
|
||||
default_value = cast(
|
||||
dict[str, Any], {} if v.default == inspect._empty else dump(v.default)
|
||||
"dict[str, Any]", {} if v.default == inspect._empty else dump(v.default)
|
||||
)
|
||||
default_value.pop("full_access_path", None)
|
||||
signature["parameters"][k] = {
|
||||
@ -385,7 +385,7 @@ def set_nested_value_by_path(
|
||||
current_dict, path_part, allow_append=False
|
||||
)
|
||||
current_dict = cast(
|
||||
dict[Any, SerializedObject],
|
||||
"dict[Any, SerializedObject]",
|
||||
next_level_serialized_object["value"],
|
||||
)
|
||||
|
||||
@ -426,7 +426,7 @@ def get_nested_dict_by_path(
|
||||
current_dict, path_part, allow_append=False
|
||||
)
|
||||
current_dict = cast(
|
||||
dict[Any, SerializedObject],
|
||||
"dict[Any, SerializedObject]",
|
||||
next_level_serialized_object["value"],
|
||||
)
|
||||
return get_container_item_by_key(current_dict, path_parts[-1], allow_append=False)
|
||||
@ -456,7 +456,7 @@ def get_or_create_item_in_container(
|
||||
return container[key]
|
||||
except IndexError:
|
||||
if allow_add_key and key == len(container):
|
||||
cast(list[SerializedObject], container).append(
|
||||
cast("list[SerializedObject]", container).append(
|
||||
create_empty_serialized_object()
|
||||
)
|
||||
return container[key]
|
||||
@ -541,7 +541,7 @@ def get_data_paths_from_serialized_object( # noqa: C901
|
||||
|
||||
elif serialized_dict_is_nested_object(serialized_obj):
|
||||
for key, value in cast(
|
||||
dict[str, SerializedObject], serialized_obj["value"]
|
||||
"dict[str, SerializedObject]", serialized_obj["value"]
|
||||
).items():
|
||||
# Serialized dictionaries need to have a different new_path than nested
|
||||
# classes
|
||||
@ -628,13 +628,13 @@ def add_prefix_to_full_access_path(
|
||||
|
||||
if isinstance(serialized_obj["value"], list):
|
||||
for value in serialized_obj["value"]:
|
||||
add_prefix_to_full_access_path(cast(SerializedObject, value), prefix)
|
||||
add_prefix_to_full_access_path(cast("SerializedObject", value), prefix)
|
||||
|
||||
elif isinstance(serialized_obj["value"], dict):
|
||||
for value in cast(
|
||||
dict[str, SerializedObject], serialized_obj["value"]
|
||||
"dict[str, SerializedObject]", serialized_obj["value"]
|
||||
).values():
|
||||
add_prefix_to_full_access_path(cast(SerializedObject, value), prefix)
|
||||
add_prefix_to_full_access_path(cast("SerializedObject", value), prefix)
|
||||
except (TypeError, KeyError, AttributeError):
|
||||
# passed dictionary is not a serialized object
|
||||
pass
|
||||
|
@ -2,8 +2,9 @@ import threading
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
import pydase
|
||||
import pytest
|
||||
|
||||
import pydase
|
||||
from pydase.client.proxy_loader import ProxyAttributeError
|
||||
|
||||
|
||||
@ -52,6 +53,7 @@ def pydase_client() -> Generator[pydase.Client, None, Any]:
|
||||
|
||||
yield client
|
||||
|
||||
client.disconnect()
|
||||
server.handle_exit()
|
||||
thread.join()
|
||||
|
||||
@ -166,9 +168,11 @@ def test_context_manager(pydase_client: pydase.Client) -> None:
|
||||
def test_client_id(
|
||||
pydase_client: pydase.Client, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
import socket
|
||||
|
||||
pydase.Client(url="ws://localhost:9999")
|
||||
|
||||
assert "Client [sid=" in caplog.text
|
||||
assert f"Client [id={socket.gethostname()}]" in caplog.text
|
||||
caplog.clear()
|
||||
|
||||
pydase.Client(url="ws://localhost:9999", client_id="my_service")
|
||||
|
@ -2,27 +2,26 @@ import threading
|
||||
from collections.abc import Callable, Generator
|
||||
from typing import Any
|
||||
|
||||
import pydase
|
||||
import pytest
|
||||
import socketio.exceptions
|
||||
|
||||
import pydase
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def pydase_restartable_server() -> (
|
||||
Generator[
|
||||
tuple[
|
||||
pydase.Server,
|
||||
threading.Thread,
|
||||
pydase.DataService,
|
||||
Callable[
|
||||
[pydase.Server, threading.Thread, pydase.DataService],
|
||||
tuple[pydase.Server, threading.Thread],
|
||||
],
|
||||
def pydase_restartable_server() -> Generator[
|
||||
tuple[
|
||||
pydase.Server,
|
||||
threading.Thread,
|
||||
pydase.DataService,
|
||||
Callable[
|
||||
[pydase.Server, threading.Thread, pydase.DataService],
|
||||
tuple[pydase.Server, threading.Thread],
|
||||
],
|
||||
None,
|
||||
Any,
|
||||
]
|
||||
):
|
||||
],
|
||||
None,
|
||||
Any,
|
||||
]:
|
||||
class MyService(pydase.DataService):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
@ -62,9 +61,6 @@ def pydase_restartable_server() -> (
|
||||
|
||||
yield server, thread, service_instance, restart
|
||||
|
||||
server.handle_exit()
|
||||
thread.join()
|
||||
|
||||
|
||||
def test_reconnection(
|
||||
pydase_restartable_server: tuple[
|
||||
@ -105,3 +101,6 @@ def test_reconnection(
|
||||
# the service proxies successfully reconnect and get the new service name
|
||||
assert client.proxy.name == "New service name"
|
||||
assert client_2.proxy.name == "New service name"
|
||||
|
||||
server.handle_exit()
|
||||
thread.join()
|
||||
|
@ -7,7 +7,7 @@ from pydase.task.autostart import autostart_service_tasks
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio(loop_scope="function")
|
||||
async def test_reconnection(caplog: LogCaptureFixture) -> None:
|
||||
class MyService(pydase.components.device_connection.DeviceConnection):
|
||||
def __init__(
|
||||
|
@ -1,8 +1,9 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pydase
|
||||
import pytest
|
||||
|
||||
import pydase
|
||||
from pydase.data_service.data_service_observer import DataServiceObserver
|
||||
from pydase.data_service.state_manager import StateManager
|
||||
from pydase.utils.serialization.serializer import SerializationError, dump
|
||||
@ -241,3 +242,42 @@ def test_read_only_dict_property(caplog: pytest.LogCaptureFixture) -> None:
|
||||
service_instance._dict_attr["dotted.key"] = 2.0
|
||||
|
||||
assert "'dict_attr[\"dotted.key\"]' changed to '2.0'" in caplog.text
|
||||
|
||||
|
||||
def test_dependency_as_function_argument(caplog: pytest.LogCaptureFixture) -> None:
|
||||
class MyObservable(pydase.DataService):
|
||||
some_int = 0
|
||||
|
||||
@property
|
||||
def other_int(self) -> int:
|
||||
return self.add_one(self.some_int)
|
||||
|
||||
def add_one(self, value: int) -> int:
|
||||
return value + 1
|
||||
|
||||
service_instance = MyObservable()
|
||||
state_manager = StateManager(service=service_instance)
|
||||
DataServiceObserver(state_manager)
|
||||
|
||||
service_instance.some_int = 1337
|
||||
|
||||
assert "'other_int' changed to '1338'" in caplog.text
|
||||
|
||||
|
||||
def test_property_starting_with_dependency_name(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
class MyObservable(pydase.DataService):
|
||||
my_int = 0
|
||||
|
||||
@property
|
||||
def my_int_2(self) -> int:
|
||||
return self.my_int + 1
|
||||
|
||||
service_instance = MyObservable()
|
||||
state_manager = StateManager(service=service_instance)
|
||||
DataServiceObserver(state_manager)
|
||||
|
||||
service_instance.my_int = 1337
|
||||
|
||||
assert "'my_int_2' changed to '1338'" in caplog.text
|
||||
|
@ -1,8 +1,9 @@
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
import pydase
|
||||
import pytest
|
||||
|
||||
import pydase
|
||||
from pydase.observer_pattern.observable.decorators import validate_set
|
||||
|
||||
|
||||
@ -17,7 +18,10 @@ def linspace(start: float, stop: float, n: int):
|
||||
|
||||
def asyncio_loop_thread(loop: asyncio.AbstractEventLoop) -> None:
|
||||
asyncio.set_event_loop(loop)
|
||||
loop.run_forever()
|
||||
try:
|
||||
loop.run_forever()
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
|
||||
def test_validate_set_precision(caplog: pytest.LogCaptureFixture) -> None:
|
||||
@ -89,10 +93,10 @@ def test_validate_set_timeout(caplog: pytest.LogCaptureFixture) -> None:
|
||||
def value(self, value: float) -> None:
|
||||
self.loop.create_task(self.set_value(value))
|
||||
|
||||
async def set_value(self, value) -> None:
|
||||
async def set_value(self, value: float) -> None:
|
||||
for i in linspace(self._value, value, 10):
|
||||
self._value = i
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
class Service(pydase.DataService):
|
||||
def __init__(self) -> None:
|
||||
@ -104,7 +108,7 @@ def test_validate_set_timeout(caplog: pytest.LogCaptureFixture) -> None:
|
||||
return self._driver.value
|
||||
|
||||
@value_1.setter
|
||||
@validate_set(timeout=0.5)
|
||||
@validate_set(timeout=0.01)
|
||||
def value_1(self, value: float) -> None:
|
||||
self._driver.value = value
|
||||
|
||||
@ -113,7 +117,7 @@ def test_validate_set_timeout(caplog: pytest.LogCaptureFixture) -> None:
|
||||
return self._driver.value
|
||||
|
||||
@value_2.setter
|
||||
@validate_set(timeout=1)
|
||||
@validate_set(timeout=0.11)
|
||||
def value_2(self, value: float) -> None:
|
||||
self._driver.value = value
|
||||
|
||||
|
@ -4,12 +4,13 @@ from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
import pydase
|
||||
import pytest
|
||||
|
||||
import pydase
|
||||
from pydase.utils.serialization.deserializer import Deserializer
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture(scope="module")
|
||||
def pydase_server() -> Generator[None, None, None]:
|
||||
class SubService(pydase.DataService):
|
||||
name = "SubService"
|
||||
@ -52,6 +53,9 @@ def pydase_server() -> Generator[None, None, None]:
|
||||
|
||||
yield
|
||||
|
||||
server.handle_exit()
|
||||
thread.join()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"access_path, expected",
|
||||
@ -107,7 +111,7 @@ def pydase_server() -> Generator[None, None, None]:
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_get_value(
|
||||
access_path: str,
|
||||
expected: dict[str, Any],
|
||||
@ -179,7 +183,7 @@ async def test_get_value(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_update_value(
|
||||
access_path: str,
|
||||
new_value: dict[str, Any],
|
||||
@ -219,7 +223,7 @@ async def test_update_value(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_trigger_method(
|
||||
access_path: str,
|
||||
expected: Any,
|
||||
@ -278,7 +282,7 @@ async def test_trigger_method(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_client_information_logging(
|
||||
headers: dict[str, str],
|
||||
log_id: str,
|
||||
|
@ -2,13 +2,14 @@ import threading
|
||||
from collections.abc import Generator
|
||||
from typing import Any
|
||||
|
||||
import pydase
|
||||
import pytest
|
||||
import socketio
|
||||
|
||||
import pydase
|
||||
from pydase.utils.serialization.deserializer import Deserializer
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@pytest.fixture(scope="module")
|
||||
def pydase_server() -> Generator[None, None, None]:
|
||||
class SubService(pydase.DataService):
|
||||
name = "SubService"
|
||||
@ -51,6 +52,9 @@ def pydase_server() -> Generator[None, None, None]:
|
||||
|
||||
yield
|
||||
|
||||
server.handle_exit()
|
||||
thread.join()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"access_path, expected",
|
||||
@ -106,7 +110,7 @@ def pydase_server() -> Generator[None, None, None]:
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_get_value(
|
||||
access_path: str,
|
||||
expected: dict[str, Any],
|
||||
@ -181,7 +185,7 @@ async def test_get_value(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_update_value(
|
||||
access_path: str,
|
||||
new_value: dict[str, Any],
|
||||
@ -226,7 +230,7 @@ async def test_update_value(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_trigger_method(
|
||||
access_path: str,
|
||||
expected: Any,
|
||||
@ -291,7 +295,7 @@ async def test_trigger_method(
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.asyncio()
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_client_information_logging(
|
||||
headers: dict[str, str],
|
||||
log_id: str,
|
||||
|
@ -1,19 +1,20 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import pydase
|
||||
import pytest
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
import pydase
|
||||
from pydase.data_service.data_service_observer import DataServiceObserver
|
||||
from pydase.data_service.state_manager import StateManager
|
||||
from pydase.task.autostart import autostart_service_tasks
|
||||
from pydase.task.decorator import task
|
||||
from pydase.task.task_status import TaskStatus
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
logger = logging.getLogger("pydase")
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio()
|
||||
async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
|
||||
class MyService(pydase.DataService):
|
||||
@task()
|
||||
@ -28,11 +29,11 @@ async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
|
||||
DataServiceObserver(state_manager)
|
||||
|
||||
autostart_service_tasks(service_instance)
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
|
||||
|
||||
service_instance.my_task.start()
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
assert service_instance.my_task.status == TaskStatus.RUNNING
|
||||
|
||||
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
|
||||
@ -40,12 +41,12 @@ async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
|
||||
caplog.clear()
|
||||
|
||||
service_instance.my_task.stop()
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
|
||||
assert "Task 'my_task' was cancelled" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio()
|
||||
async def test_autostart_task(caplog: LogCaptureFixture) -> None:
|
||||
class MyService(pydase.DataService):
|
||||
@task(autostart=True)
|
||||
@ -61,13 +62,16 @@ async def test_autostart_task(caplog: LogCaptureFixture) -> None:
|
||||
|
||||
autostart_service_tasks(service_instance)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
assert service_instance.my_task.status == TaskStatus.RUNNING
|
||||
|
||||
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
|
||||
|
||||
service_instance.my_task.stop()
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_nested_list_autostart_task(
|
||||
caplog: LogCaptureFixture,
|
||||
) -> None:
|
||||
@ -86,7 +90,7 @@ async def test_nested_list_autostart_task(
|
||||
DataServiceObserver(state_manager)
|
||||
autostart_service_tasks(service_instance)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
assert service_instance.sub_services_list[0].my_task.status == TaskStatus.RUNNING
|
||||
assert service_instance.sub_services_list[1].my_task.status == TaskStatus.RUNNING
|
||||
|
||||
@ -99,8 +103,12 @@ async def test_nested_list_autostart_task(
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
service_instance.sub_services_list[0].my_task.stop()
|
||||
service_instance.sub_services_list[1].my_task.stop()
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_nested_dict_autostart_task(
|
||||
caplog: LogCaptureFixture,
|
||||
) -> None:
|
||||
@ -120,7 +128,7 @@ async def test_nested_dict_autostart_task(
|
||||
|
||||
autostart_service_tasks(service_instance)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
assert (
|
||||
service_instance.sub_services_dict["first"].my_task.status == TaskStatus.RUNNING
|
||||
@ -139,8 +147,12 @@ async def test_nested_dict_autostart_task(
|
||||
in caplog.text
|
||||
)
|
||||
|
||||
service_instance.sub_services_dict["first"].my_task.stop()
|
||||
service_instance.sub_services_dict["second"].my_task.stop()
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_manual_start_with_multiple_service_instances(
|
||||
caplog: LogCaptureFixture,
|
||||
) -> None:
|
||||
@ -161,7 +173,7 @@ async def test_manual_start_with_multiple_service_instances(
|
||||
|
||||
autostart_service_tasks(service_instance)
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
assert (
|
||||
service_instance.sub_services_list[0].my_task.status == TaskStatus.NOT_RUNNING
|
||||
@ -291,7 +303,7 @@ async def test_manual_start_with_multiple_service_instances(
|
||||
assert "Task 'my_task' was cancelled" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio()
|
||||
async def test_restart_on_exception(caplog: LogCaptureFixture) -> None:
|
||||
class MyService(pydase.DataService):
|
||||
@task(restart_on_exception=True, restart_sec=0.1)
|
||||
@ -312,8 +324,11 @@ async def test_restart_on_exception(caplog: LogCaptureFixture) -> None:
|
||||
assert "Task 'my_task' encountered an exception" in caplog.text
|
||||
assert "Triggered task." in caplog.text
|
||||
|
||||
service_instance.my_task.stop()
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_restart_sec(caplog: LogCaptureFixture) -> None:
|
||||
class MyService(pydase.DataService):
|
||||
@task(restart_on_exception=True, restart_sec=0.1)
|
||||
@ -334,8 +349,11 @@ async def test_restart_sec(caplog: LogCaptureFixture) -> None:
|
||||
await asyncio.sleep(0.05)
|
||||
assert "Triggered task." in caplog.text # Ensures the task restarted after 0.2s
|
||||
|
||||
service_instance.my_task.stop()
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_exceeding_start_limit_interval_sec_and_burst(
|
||||
caplog: LogCaptureFixture,
|
||||
) -> None:
|
||||
@ -359,7 +377,7 @@ async def test_exceeding_start_limit_interval_sec_and_burst(
|
||||
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio()
|
||||
async def test_non_exceeding_start_limit_interval_sec_and_burst(
|
||||
caplog: LogCaptureFixture,
|
||||
) -> None:
|
||||
@ -382,8 +400,11 @@ async def test_non_exceeding_start_limit_interval_sec_and_burst(
|
||||
assert "Task 'my_task' exceeded restart burst limit" not in caplog.text
|
||||
assert service_instance.my_task.status == TaskStatus.RUNNING
|
||||
|
||||
service_instance.my_task.stop()
|
||||
await asyncio.sleep(0.01)
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_exit_on_failure(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture
|
||||
) -> None:
|
||||
@ -408,7 +429,7 @@ async def test_exit_on_failure(
|
||||
assert "Task 'my_task' encountered an exception" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio()
|
||||
async def test_exit_on_failure_exceeding_rate_limit(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture
|
||||
) -> None:
|
||||
@ -438,7 +459,7 @@ async def test_exit_on_failure_exceeding_rate_limit(
|
||||
assert "Task 'my_task' encountered an exception" in caplog.text
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="function")
|
||||
@pytest.mark.asyncio()
|
||||
async def test_gracefully_finishing_task(
|
||||
monkeypatch: pytest.MonkeyPatch, caplog: LogCaptureFixture
|
||||
) -> None:
|
||||
|
@ -4,5 +4,5 @@ import toml
|
||||
|
||||
def test_project_version() -> None:
|
||||
pyproject = toml.load("pyproject.toml")
|
||||
pydase_pyroject_version = pyproject["tool"]["poetry"]["version"]
|
||||
pydase_pyroject_version = pyproject["project"]["version"]
|
||||
assert pydase.version.__version__ == pydase_pyroject_version
|
||||
|
@ -3,15 +3,15 @@ from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import pytest
|
||||
|
||||
import pydase
|
||||
import pydase.units as u
|
||||
import pytest
|
||||
from pydase.components.coloured_enum import ColouredEnum
|
||||
from pydase.task.task_status import TaskStatus
|
||||
from pydase.utils.decorators import frontend
|
||||
from pydase.utils.serialization.serializer import (
|
||||
SerializationPathError,
|
||||
SerializedObject,
|
||||
add_prefix_to_full_access_path,
|
||||
dump,
|
||||
generate_serialized_data_paths,
|
||||
@ -21,6 +21,7 @@ from pydase.utils.serialization.serializer import (
|
||||
serialized_dict_is_nested_object,
|
||||
set_nested_value_by_path,
|
||||
)
|
||||
from pydase.utils.serialization.types import SerializedObject
|
||||
|
||||
|
||||
class MyEnum(enum.Enum):
|
||||
@ -207,7 +208,7 @@ def test_ColouredEnum_serialize() -> None:
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio(scope="module")
|
||||
@pytest.mark.asyncio(loop_scope="module")
|
||||
async def test_method_serialization() -> None:
|
||||
class ClassWithMethod(pydase.DataService):
|
||||
def some_method(self) -> str:
|
||||
@ -252,7 +253,7 @@ def test_methods_with_type_hints() -> None:
|
||||
def method_with_type_hint(some_argument: int) -> None:
|
||||
pass
|
||||
|
||||
def method_with_union_type_hint(some_argument: int | float) -> None:
|
||||
def method_with_union_type_hint(some_argument: int | float) -> None: # noqa: PYI041
|
||||
pass
|
||||
|
||||
assert dump(method_without_type_hint) == {
|
||||
|
Loading…
x
Reference in New Issue
Block a user