updates documentation

- using material theme instead of readthedocs
- introducing "Interacting with pydase Services" guide
    - restful api docs
    - auto-generated frontend
    - pydase.Client
This commit is contained in:
Mose Müller
2024-07-29 11:08:27 +02:00
parent 9ce0c93954
commit baad1268e8
14 changed files with 577 additions and 123 deletions

View File

@@ -0,0 +1,167 @@
# Auto-generated Frontend
`pydase` automatically generates a frontend interface based on your service definition, representing the current state and controls of the service.
It simplifies the process of visualization and control of the data and devices managed by your `pydase` service, making it accessible to both developers and end-users.
Through the integration of Socket.IO, the frontend provides real-time updates, reflecting changes as they occur and allowing for immediate interaction with the backend.
## Accessing the Frontend
You can access the auto-generated frontend by navigating to the hostname of the device the service is hosted on, followed by the exposed port:
```
http://<hostname>:<port>/
```
The frontend uses a component-based approach, representing various data types and control mechanisms as distinct UI components. For more information about this, please refer to [Components Guide](../Components.md).
## Customization Options
`pydase` allows you to enhance the user experience by customizing the web interface's appearance through
1. a custom CSS file, and
2. tailoring the frontend component layout and display style.
For more advanced customization, you can provide a completely custom frontend source.
### Custom CSS Styling
You can apply your own styles globally across the web interface by passing a custom CSS file to the server during initialization.
Here's how you can use this feature:
1. Prepare your custom CSS file with the desired styles.
2. When initializing your server, use the `css` parameter of the `Server` class to specify the path to your custom CSS file.
```python
from pydase import Server, DataService
class MyService(DataService):
# ... your service definition ...
if __name__ == "__main__":
service = MyService()
server = Server(service, css="path/to/your/custom.css").run()
```
This will apply the styles defined in `custom.css` to the web interface, allowing you to maintain branding consistency or improve visual accessibility.
Please ensure that the CSS file path is accessible from the server's running location. Relative or absolute paths can be used depending on your setup.
### Tailoring Frontend Component Layout
You can customize the display names, visibility, and order of components via the `web_settings.json` file.
Each key in the file corresponds to the full access path of public attributes, properties, and methods of the exposed service, using dot-notation.
- **Custom Display Names**: Modify the `"displayName"` value in the file to change how each component appears in the frontend.
- **Control Component Visibility**: Utilize the `"display"` key-value pair to control whether a component is rendered in the frontend. Set the value to `true` to make the component visible or `false` to hide it.
- **Adjustable Component Order**: The `"displayOrder"` values determine the order of components. Alter these values to rearrange the components as desired. The value defaults to [`Number.MAX_SAFE_INTEGER`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER).
The `web_settings.json` file will be stored in the directory specified by `SERVICE_CONFIG_DIR`. You can generate a `web_settings.json` file by setting the `GENERATE_WEB_SETTINGS` to `True`. For more information, see the [configuration section](#configuring-pydase-via-environment-variables).
For example, styling the following service
```python
import pydase
class Device(pydase.DataService):
name = "My Device"
temperature = 1.0
power = 1
class Service(pydase.DataService):
device = Device()
state = "RUNNING"
if __name__ == "__main__":
pydase.Server(Service()).run()
```
with the following `web_settings.json`
```json
{
"device": {
"displayName": "My Device",
"displayOrder": 1
},
"device.name": {
"display": false
},
"device.power": {
"displayName": "Power",
"displayOrder": 1
},
"device.temperature": {
"displayName": "Temperature",
"displayOrder": 0
},
"state": {
"displayOrder": 0
}
}
```
looks like this:
![Tailoring frontend component layout](../../images/Tailoring frontend component layout.png)
### Specifying a Custom Frontend Source
To further customize your web interface, you can provide a custom frontend source.
By specifying the `frontend_src` parameter when initializing the server, you can host a tailored frontend application:
```python
from pathlib import Path
import pydase
class MyService(pydase.DataService):
# Service definition
if __name__ == "__main__":
service = MyService()
pydase.Server(
service,
frontend_src=Path("path/to/your/frontend/directory"),
).run()
```
`pydase` expects a directory structured as follows:
```bash title="Frontend directory structure"
<your_frontend_directory>
├── assets
│   └── ...
└── index.html
```
Any CSS, js, image or other files need to be put into the assets folder for the web server to be able to provide access to it.
#### Example: Custom React Frontend
You can use vite to generate a react app template:
```bash
npm create vite@latest my-react-app -- --template react
```
*TODO: Add some useful information here...*
To deploy the custom react frontend, build it with
```bash
npm run build
```
and pass the relative path of the output directory to the `frontend_src` parameter of the `pydase.Server`.
**Note** that you have to make sure that all the generated files (except the `index.html`) are in the `assets` folder. In the react app, you can achieve this by not using the `public` folder, but instead using e.g. `src/assets`.

View File

@@ -0,0 +1,45 @@
# Python Client
You can connect to the service using the `pydase.Client`. Below is an example of how to establish a connection to a service and interact with it:
```python
import pydase
# Replace the hostname and port with the IP address and the port of the machine
# where the service is running, respectively
client_proxy = pydase.Client(hostname="<ip_addr>", port=8001).proxy
# Interact with the service attributes as if they were local
client_proxy.voltage = 5.0
print(client_proxy.voltage) # Expected output: 5.0
```
This example demonstrates setting and retrieving the `voltage` attribute through the client proxy.
The proxy acts as a local representative of the remote service, enabling straightforward interaction.
The proxy class dynamically synchronizes with the server's exposed attributes. This synchronization allows the proxy to be automatically updated with any attributes or methods that the server exposes, essentially mirroring the server's API. This dynamic updating enables users to interact with the remote service as if they were working with a local object.
## Tab Completion Support
In interactive environments such as Python interpreters and Jupyter notebooks, the proxy class supports tab completion, which allows users to explore available methods and attributes.
## Integration within Other Services
You can also integrate a client proxy within another service. Here's how you can set it up:
```python
import pydase
class MyService(pydase.DataService):
# Initialize the client without blocking the constructor
proxy = pydase.Client(hostname="<ip_addr>", port=8001, block_until_connected=False).proxy
if __name__ == "__main__":
service = MyService()
# Create a server that exposes this service; adjust the web_port as needed
server = pydase.Server(service, web_port=8002). run()
```
In this setup, the `MyService` class has a `proxy` attribute that connects to a `pydase` service located at `<ip_addr>:8001`.
The `block_until_connected=False` argument allows the service to start up even if the initial connection attempt fails.
This configuration is particularly useful in distributed systems where services may start in any order.

View File

@@ -0,0 +1,14 @@
# RESTful API
The `pydase` RESTful API allows for standard HTTP-based interactions and provides access to various functionalities through specific routes. This is particularly useful for integrating `pydase` services with other applications or for scripting and automation.
To help developers understand and utilize the API, we provide an OpenAPI specification. This specification describes the available endpoints and corresponding request/response formats.
## OpenAPI Specification
**Base URL**:
```
http://<hostname>:<port>/api/
```
<swagger-ui src="./openapi.yaml"/>

View File

@@ -0,0 +1,81 @@
# Interacting with `pydase` Services
`pydase` offers multiple ways for users to interact with the services they create, providing flexibility and convenience for different use cases. This section outlines the primary interaction methods available, including an auto-generated frontend, a RESTful API, and a Python client based on Socket.IO.
{%
include-markdown "./Auto-generated Frontend.md"
heading-offset=1
%}
{%
include-markdown "./RESTful API.md"
heading-offset=1
%}
{%
include-markdown "./Python Client.md"
heading-offset=1
%}
<!-- ## 2. **Socket.IO for Real-Time Updates** -->
<!-- For scenarios requiring real-time data updates, `pydase` includes a Socket.IO server. This feature is ideal for applications where live data tracking is crucial, such as monitoring systems or interactive dashboards. -->
<!---->
<!-- ### Key Features: -->
<!-- - **Live Data Streams**: Receive real-time updates for data changes. -->
<!-- - **Event-Driven Communication**: Utilize event-based messaging to push updates and handle client actions. -->
<!---->
<!-- ### Example Usage: -->
<!-- Clients can connect to the Socket.IO server to receive updates: -->
<!-- ```javascript -->
<!-- var socket = io.connect('http://<hostname>:<port>'); -->
<!-- socket.on('<event_name>', function(data) { -->
<!-- console.log(data); -->
<!-- }); -->
<!-- ``` -->
<!---->
<!-- **Use Cases:** -->
<!---->
<!-- - Real-time monitoring and alerts -->
<!-- - Live data visualization -->
<!-- - Collaborative applications -->
<!---->
<!-- ## 3. **Auto-Generated Frontend** -->
<!-- `pydase` automatically generates a web frontend based on the service definitions. This frontend is a convenient interface for interacting with the service, especially for users who prefer a graphical interface over command-line or code-based interactions. -->
<!---->
<!-- ### Key Features: -->
<!-- - **User-Friendly Interface**: Intuitive and easy to use, with real-time interaction capabilities. -->
<!-- - **Customizable**: Adjust the frontend's appearance and functionality to suit specific needs. -->
<!---->
<!-- ### Accessing the Frontend: -->
<!-- Once the service is running, access the frontend via a web browser: -->
<!-- ``` -->
<!-- http://<hostname>:<port> -->
<!-- ``` -->
<!---->
<!-- **Use Cases:** -->
<!---->
<!-- - End-user interfaces for data control and visualization -->
<!-- - Rapid prototyping and testing -->
<!-- - Demonstrations and training -->
<!---->
<!-- ## 4. **Python Client** -->
<!-- `pydase` also provides a Python client for programmatic interactions. This client is particularly useful for developers who want to integrate `pydase` services into other Python applications or automate interactions. -->
<!---->
<!-- ### Key Features: -->
<!-- - **Direct Interaction**: Call methods and access properties as if they were local. -->
<!-- - **Tab Completion**: Supports tab completion in interactive environments like Jupyter notebooks. -->
<!---->
<!-- ### Example Usage: -->
<!-- ```python -->
<!-- import pydase -->
<!---->
<!-- client = pydase.Client(hostname="<ip_addr>", port=8001) -->
<!-- service = client.proxy -->
<!-- service.some_method() -->
<!-- ``` -->
<!---->
<!-- **Use Cases:** -->
<!---->
<!-- - Integrating with other Python applications -->
<!-- - Automation and scripting -->
<!-- - Data analysis and manipulation -->

View File

@@ -0,0 +1,257 @@
openapi: 3.1.0
info:
version: 1.0.0
title: pydase API
tags:
- name: /api/v1
description: Version 1
paths:
/api/v1/get_value:
get:
tags:
- /api/v1
summary: Get the value of an existing attribute.
description: Get the value of an existing attribute by full access path.
operationId: getValue
parameters:
- in: query
name: access_path
schema:
type: string
example: device.channel[0].voltage
required: true
description: Full access path of the service attribute.
responses:
'200':
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedAttribute'
examples:
Exists:
summary: Attribute exists
value:
docs: My documentation string.
full_access_path: device.channel[0].voltage
readonly: false
type: float
value: 12.1
DoesNotExist:
summary: Attribute or does not exist
value:
docs: null
full_access_path: device.channel[0].voltage
readonly: false
type: "None"
value: null
/api/v1/update_value:
put:
tags:
- /api/v1
summary: Update an existing attribute.
description: Update an existing attribute by full access path.
operationId: updateValue
requestBody:
description: Update an existent attribute in the service
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateValue'
required: true
responses:
'200':
description: Successful Operation
'400':
description: Could not Update Attribute
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedException'
examples:
List:
summary: List out of index
value:
docs: null
full_access_path: ""
name: SerializationPathError
readonly: false
type: Exception
value: "Index '2': list index out of range"
Attribute:
summary: Attribute or does not exist
value:
docs: null
full_access_path: ""
name: SerializationPathError
readonly: false
type: Exception
value: "Key 'invalid_attribute': 'invalid_attribute'."
/api/v1/trigger_method:
put:
tags:
- /api/v1
summary: Trigger method.
description: Trigger method with by full access path with provided args and kwargs.
operationId: triggerMethod
requestBody:
description: Update an existent attribute in the service
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerMethod'
required: true
responses:
'200':
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/SerializedAttribute'
examples:
NoneReturn:
summary: Function returns None
value:
docs: null
full_access_path: ""
readonly: false
type: "NoneType"
value: null
FloatReturn:
summary: Function returns float
value:
docs: null
full_access_path: ""
readonly: false
type: "float"
value: 23.2
components:
schemas:
UpdateValue:
required:
- access_path
- value
type: object
properties:
access_path:
type: string
example: device.channel[0].voltage
value:
$ref: '#/components/schemas/SerializedValue'
TriggerMethod:
required:
- access_path
type: object
properties:
access_path:
type: string
example: device.channel[0].voltage
args:
type: object
required:
- type
- value
- full_access_path
properties:
full_access_path:
type: string
example: ""
type:
type: string
enum:
- list
value:
type: array
items:
$ref: '#/components/schemas/SerializedValue'
kwargs:
type: object
required:
- type
- value
- full_access_path
properties:
full_access_path:
type: string
example: ""
type:
type: string
enum:
- dict
value:
type: object
additionalProperties:
$ref: '#/components/schemas/SerializedValue'
SerializedValue:
required:
- full_access_path
- type
- value
type: object
properties:
docs:
type: string | null
example: null
full_access_path:
type: string
example: ""
readonly:
type: boolean
example: false
type:
type: string
example: float
value:
type: any
example: 22.0
SerializedAttribute:
required:
- full_access_path
- type
- value
type: object
properties:
docs:
type: string | null
example: My documentation string.
full_access_path:
type: string
example: device.channel[0].voltage
readonly:
type: boolean
example: false
type:
type: string
example: float
value:
type: any
example: 22.0
SerializedException:
required:
- full_access_path
- type
- value
type: object
properties:
docs:
type: string | null
example: Raised when the access path does not correspond to a valid attribute.
full_access_path:
type: string
example: ""
name:
type: string
example: SerializationPathError
readonly:
type: boolean
example: false
type:
type: string
example: Exception
value:
type: string
examples:
value:
"Index '2': list index out of range"
some:
"Index '2': list index out of range"