mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-04-20 00:10:03 +02:00
Merge pull request #41 from tiqi-group/39-feat-add-customcss-option-to-pydaseserver
adds custom css option to pydase.Server
This commit is contained in:
commit
d334ec5284
80
README.md
80
README.md
@ -21,6 +21,7 @@
|
|||||||
- [`NumberSlider`](#numberslider)
|
- [`NumberSlider`](#numberslider)
|
||||||
- [`ColouredEnum`](#colouredenum)
|
- [`ColouredEnum`](#colouredenum)
|
||||||
- [Extending with New Components](#extending-with-new-components)
|
- [Extending with New Components](#extending-with-new-components)
|
||||||
|
- [Customizing Web Interface Style](#customizing-web-interface-style)
|
||||||
- [Understanding Service Persistence](#understanding-service-persistence)
|
- [Understanding Service Persistence](#understanding-service-persistence)
|
||||||
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
|
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
|
||||||
- [Understanding Units in pydase](#understanding-units-in-pydase)
|
- [Understanding Units in pydase](#understanding-units-in-pydase)
|
||||||
@ -32,18 +33,21 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
<!-- no toc -->
|
<!-- no toc -->
|
||||||
* [Simple data service definition through class-based interface](#defining-a-dataService)
|
- [Simple data service definition through class-based interface](#defining-a-dataService)
|
||||||
* [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
|
- [Integrated web interface for interactive access and control of your data service](#accessing-the-web-interface)
|
||||||
* [Support for `rpyc` connections, allowing for programmatic control and interaction with your service](#connecting-to-the-service-using-rpyc)
|
- [Support for `rpyc` connections, allowing for programmatic control and interaction with your service](#connecting-to-the-service-using-rpyc)
|
||||||
* [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
|
- [Component system bridging Python backend with frontend visual representation](#understanding-the-component-system)
|
||||||
* [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
- [Customizable styling for the web interface through user-defined CSS](#customizing-web-interface-style)
|
||||||
* [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
|
||||||
* [Support for units](#understanding-units-in-pydase)
|
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
|
||||||
|
- [Support for units](#understanding-units-in-pydase)
|
||||||
<!-- * Event-based callback functionality for real-time updates
|
<!-- * Event-based callback functionality for real-time updates
|
||||||
* Support for additional servers for specific use-cases -->
|
- Support for additional servers for specific use-cases -->
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
<!--installation-start-->
|
<!--installation-start-->
|
||||||
|
|
||||||
Install pydase using [`poetry`](https://python-poetry.org/):
|
Install pydase using [`poetry`](https://python-poetry.org/):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@ -55,10 +59,13 @@ or `pip`:
|
|||||||
```bash
|
```bash
|
||||||
pip install pydase
|
pip install pydase
|
||||||
```
|
```
|
||||||
|
|
||||||
<!--installation-end-->
|
<!--installation-end-->
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
<!--usage-start-->
|
<!--usage-start-->
|
||||||
|
|
||||||
Using `pydase` involves three main steps: defining a `DataService` subclass, running the server, and then connecting to the service either programmatically using `rpyc` or through the web interface.
|
Using `pydase` involves three main steps: defining a `DataService` subclass, running the server, and then connecting to the service either programmatically using `rpyc` or through the web interface.
|
||||||
|
|
||||||
### Defining a DataService
|
### Defining a DataService
|
||||||
@ -159,6 +166,7 @@ print(client.voltage) # prints 5.0
|
|||||||
```
|
```
|
||||||
|
|
||||||
In this example, replace `<ip_addr>` with the IP address of the machine where the service is running. After establishing a connection, you can interact with the service attributes as if they were local attributes.
|
In this example, replace `<ip_addr>` with the IP address of the machine where the service is running. After establishing a connection, you can interact with the service attributes as if they were local attributes.
|
||||||
|
|
||||||
<!--usage-end-->
|
<!--usage-end-->
|
||||||
|
|
||||||
## Understanding the Component System
|
## Understanding the Component System
|
||||||
@ -226,9 +234,10 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
|
**Note** that defining classes within `DataService` classes is not supported (see [this issue](https://github.com/tiqi-group/pydase/issues/16)).
|
||||||
|
|
||||||
### Custom Components (`pydase.components`)
|
### Custom Components (`pydase.components`)
|
||||||
|
|
||||||
The custom components in `pydase` have two main parts:
|
The custom components in `pydase` have two main parts:
|
||||||
|
|
||||||
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
|
- A **Python Component Class** in the backend, implementing the logic needed to set, update, and manage the component's state and data.
|
||||||
@ -276,7 +285,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
#### `NumberSlider`
|
#### `NumberSlider`
|
||||||
|
|
||||||
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
|
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
|
||||||
|
|
||||||
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
|
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
|
||||||
|
|
||||||
@ -342,6 +351,31 @@ Users can also extend the library by creating custom components. This involves d
|
|||||||
|
|
||||||
<!-- Component User Guide End -->
|
<!-- Component User Guide End -->
|
||||||
|
|
||||||
|
## Customizing Web Interface Style
|
||||||
|
|
||||||
|
`pydase` allows you to enhance the user experience by customizing the web interface's appearance. 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 Device(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.
|
||||||
|
|
||||||
## Understanding Service Persistence
|
## Understanding Service Persistence
|
||||||
|
|
||||||
`pydase` allows you to easily persist the state of your service by saving it to a file. This is especially useful when you want to maintain the service's state across different runs.
|
`pydase` allows you to easily persist the state of your service by saving it to a file. This is especially useful when you want to maintain the service's state across different runs.
|
||||||
@ -483,25 +517,25 @@ You can change the log level of the logger by either
|
|||||||
|
|
||||||
1. (RECOMMENDED) setting the `ENVIRONMENT` environment variable to "production" or "development"
|
1. (RECOMMENDED) setting the `ENVIRONMENT` environment variable to "production" or "development"
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
ENVIRONMENT="production" python -m <module_using_pydase>
|
ENVIRONMENT="production" python -m <module_using_pydase>
|
||||||
```
|
```
|
||||||
|
|
||||||
The production environment will only log messages above "INFO", the development environment (default) logs everything above "DEBUG".
|
The production environment will only log messages above "INFO", the development environment (default) logs everything above "DEBUG".
|
||||||
|
|
||||||
2. calling the `pydase.utils.logging.setup_logging` function with the desired log level
|
2. calling the `pydase.utils.logging.setup_logging` function with the desired log level
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# <your_script.py>
|
# <your_script.py>
|
||||||
import logging
|
import logging
|
||||||
from pydase.utils.logging import setup_logging
|
from pydase.utils.logging import setup_logging
|
||||||
|
|
||||||
setup_logging("INFO") # or setup_logging(logging.INFO)
|
setup_logging("INFO") # or setup_logging(logging.INFO)
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
|
|
||||||
# ... and your log
|
# ... and your log
|
||||||
logger.info("My info message.")
|
logger.info("My info message.")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
@ -129,6 +129,20 @@ const App = () => {
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data: DataServiceJSON) => dispatch({ type: 'SET_DATA', data }));
|
.then((data: DataServiceJSON) => dispatch({ type: 'SET_DATA', data }));
|
||||||
|
|
||||||
|
// Allow the user to add a custom css file
|
||||||
|
fetch(`http://${hostname}:${port}/custom.css`)
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
// If the file exists, create a link element for the custom CSS
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.href = `http://${hostname}:${port}/custom.css`;
|
||||||
|
link.type = 'text/css';
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(console.error); // Handle the error appropriately
|
||||||
|
|
||||||
socket.on('notify', onNotify);
|
socket.on('notify', onNotify);
|
||||||
socket.on('exception', onException);
|
socket.on('exception', onException);
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
|
|||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'listComponent'} id={parentPath.concat(name)}>
|
<div className={'listComponent'} id={parentPath.concat('.' + name)}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<p>Render count: {renderCount.current}</p>
|
||||||
)}
|
)}
|
||||||
|
@ -137,7 +137,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
|
|
||||||
// Set the cursor position after the component re-renders
|
// Set the cursor position after the component re-renders
|
||||||
const inputElement = document.getElementsByName(
|
const inputElement = document.getElementsByName(
|
||||||
parentPath.concat(name)
|
parentPath.concat('.' + name)
|
||||||
)[0] as HTMLInputElement;
|
)[0] as HTMLInputElement;
|
||||||
if (inputElement && cursorPosition !== null) {
|
if (inputElement && cursorPosition !== null) {
|
||||||
inputElement.setSelectionRange(cursorPosition, cursorPosition);
|
inputElement.setSelectionRange(cursorPosition, cursorPosition);
|
||||||
@ -287,7 +287,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
type="text"
|
type="text"
|
||||||
value={inputString}
|
value={inputString}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
name={parentPath.concat(name)}
|
name={parentPath.concat('.' + name)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
|
className={isInstantUpdate && !readOnly ? 'instantUpdate' : ''}
|
||||||
|
@ -55,7 +55,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'stringComponent'} id={parentPath.concat(name)}>
|
<div className={'stringComponent'} id={parentPath.concat('.' + name)}>
|
||||||
{process.env.NODE_ENV === 'development' && (
|
{process.env.NODE_ENV === 'development' && (
|
||||||
<p>Render count: {renderCount.current}</p>
|
<p>Render count: {renderCount.current}</p>
|
||||||
)}
|
)}
|
||||||
|
@ -5,6 +5,7 @@ from typing import Any, TypedDict
|
|||||||
import socketio
|
import socketio
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
from fastapi.responses import FileResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
from pydase import DataService
|
from pydase import DataService
|
||||||
@ -86,7 +87,7 @@ class WebAPI:
|
|||||||
self.__sio = sio
|
self.__sio = sio
|
||||||
self.__sio_app = socketio.ASGIApp(self.__sio)
|
self.__sio_app = socketio.ASGIApp(self.__sio)
|
||||||
|
|
||||||
def setup_fastapi_app(self) -> None: # noqa: CFQ004
|
def setup_fastapi_app(self) -> None: # noqa
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
|
|
||||||
if self.enable_CORS:
|
if self.enable_CORS:
|
||||||
@ -99,7 +100,6 @@ class WebAPI:
|
|||||||
)
|
)
|
||||||
app.mount("/ws", self.__sio_app)
|
app.mount("/ws", self.__sio_app)
|
||||||
|
|
||||||
# @app.get("/version", include_in_schema=False)
|
|
||||||
@app.get("/version")
|
@app.get("/version")
|
||||||
def version() -> str:
|
def version() -> str:
|
||||||
return __version__
|
return __version__
|
||||||
@ -116,6 +116,13 @@ class WebAPI:
|
|||||||
def service_properties() -> dict[str, Any]:
|
def service_properties() -> dict[str, Any]:
|
||||||
return self.service.serialize()
|
return self.service.serialize()
|
||||||
|
|
||||||
|
# exposing custom.css file provided by user
|
||||||
|
if self.css is not None:
|
||||||
|
|
||||||
|
@app.get("/custom.css")
|
||||||
|
async def styles():
|
||||||
|
return FileResponse(str(self.css))
|
||||||
|
|
||||||
app.mount(
|
app.mount(
|
||||||
"/",
|
"/",
|
||||||
StaticFiles(
|
StaticFiles(
|
||||||
@ -126,14 +133,6 @@ class WebAPI:
|
|||||||
|
|
||||||
self.__fastapi_app = app
|
self.__fastapi_app = app
|
||||||
|
|
||||||
def add_endpoint(self, name: str) -> None:
|
|
||||||
# your endpoint creation code
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_custom_openapi(self) -> None:
|
|
||||||
# your custom openapi generation code
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sio(self) -> socketio.AsyncServer:
|
def sio(self) -> socketio.AsyncServer:
|
||||||
return self.__sio
|
return self.__sio
|
||||||
|
Loading…
x
Reference in New Issue
Block a user