mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-19 04:31:19 +01:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a4b4f179c6 | ||
|
|
c6beca3961 | ||
|
|
2fa8240e54 | ||
|
|
369587a50c | ||
|
|
25343f6909 | ||
|
|
c136c9f3de | ||
|
|
8897c2fe4c | ||
|
|
80c5c4e99d | ||
|
|
423441a74c | ||
|
|
9ec60e3891 | ||
|
|
8bde104322 | ||
|
|
9b57b6984e | ||
|
|
e5b89f2581 | ||
|
|
ff1654e65c | ||
|
|
cded80c8e5 | ||
|
|
87a33b6293 |
2
.github/workflows/python-package.yml
vendored
2
.github/workflows/python-package.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pydase"
|
||||
version = "0.10.16"
|
||||
version = "0.10.17"
|
||||
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 = [
|
||||
{name = "Mose Müller",email = "mosemueller@gmail.com"}
|
||||
|
||||
@@ -135,6 +135,14 @@ class Server:
|
||||
autosave_interval: Interval in seconds between automatic state save events.
|
||||
If set to `None`, automatic saving is disabled. Defaults to 30 seconds.
|
||||
**kwargs: Additional keyword arguments.
|
||||
|
||||
# Advanced
|
||||
- [`post_startup`][pydase.Server.post_startup] hook:
|
||||
|
||||
This method is intended to be overridden in subclasses. It runs immediately
|
||||
after all servers (web and additional) are initialized and before entering the
|
||||
main event loop. You can use this hook to register custom logic after the
|
||||
server is fully started.
|
||||
"""
|
||||
|
||||
def __init__( # noqa: PLR0913
|
||||
@@ -174,6 +182,14 @@ class Server:
|
||||
self._state_manager.load_state()
|
||||
autostart_service_tasks(self._service)
|
||||
|
||||
self._web_server = WebServer(
|
||||
data_service_observer=self._observer,
|
||||
host=self._host,
|
||||
port=self._web_port,
|
||||
enable_frontend=self._enable_web,
|
||||
**self._kwargs,
|
||||
)
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Initializes the asyncio event loop and starts the server.
|
||||
@@ -191,6 +207,7 @@ class Server:
|
||||
logger.info("Started server process [%s]", process_id)
|
||||
|
||||
await self.startup()
|
||||
await self.post_startup()
|
||||
if self.should_exit:
|
||||
return
|
||||
await self.main_loop()
|
||||
@@ -202,6 +219,10 @@ class Server:
|
||||
self._loop.set_exception_handler(self.custom_exception_handler)
|
||||
self.install_signal_handlers()
|
||||
|
||||
server_task = self._loop.create_task(self._web_server.serve())
|
||||
server_task.add_done_callback(self._handle_server_shutdown)
|
||||
self.servers["web"] = server_task
|
||||
|
||||
for server in self._additional_servers:
|
||||
addin_server = server["server"](
|
||||
data_service_observer=self._observer,
|
||||
@@ -217,17 +238,6 @@ class Server:
|
||||
server_task = self._loop.create_task(addin_server.serve())
|
||||
server_task.add_done_callback(self._handle_server_shutdown)
|
||||
self.servers[server_name] = server_task
|
||||
if self._enable_web:
|
||||
self._web_server = WebServer(
|
||||
data_service_observer=self._observer,
|
||||
host=self._host,
|
||||
port=self._web_port,
|
||||
**self._kwargs,
|
||||
)
|
||||
server_task = self._loop.create_task(self._web_server.serve())
|
||||
|
||||
server_task.add_done_callback(self._handle_server_shutdown)
|
||||
self.servers["web"] = server_task
|
||||
|
||||
self._loop.create_task(self._state_manager.autosave())
|
||||
|
||||
@@ -258,6 +268,9 @@ class Server:
|
||||
logger.debug("Cancelling tasks")
|
||||
await self.__cancel_tasks()
|
||||
|
||||
async def post_startup(self) -> None:
|
||||
"""Override this in a subclass to register custom logic after startup."""
|
||||
|
||||
async def __cancel_servers(self) -> None:
|
||||
for server_name, task in self.servers.items():
|
||||
task.cancel()
|
||||
@@ -307,7 +320,7 @@ class Server:
|
||||
# here we exclude most kinds of exceptions from triggering this kind of shutdown
|
||||
exc = context.get("exception")
|
||||
if type(exc) not in [RuntimeError, KeyboardInterrupt, asyncio.CancelledError]:
|
||||
if self._enable_web:
|
||||
if loop.is_running():
|
||||
|
||||
async def emit_exception() -> None:
|
||||
try:
|
||||
|
||||
@@ -115,7 +115,7 @@ def setup_sio_server(
|
||||
def sio_callback(
|
||||
full_access_path: str, value: Any, cached_value_dict: SerializedObject
|
||||
) -> None:
|
||||
if cached_value_dict != {}:
|
||||
if cached_value_dict != {} and loop.is_running():
|
||||
|
||||
async def notify() -> None:
|
||||
try:
|
||||
|
||||
@@ -81,6 +81,7 @@ class WebServer:
|
||||
host: str,
|
||||
port: int,
|
||||
*,
|
||||
enable_frontend: bool = True,
|
||||
css: str | Path | None = None,
|
||||
favicon_path: str | Path | None = None,
|
||||
enable_cors: bool = True,
|
||||
@@ -97,19 +98,18 @@ class WebServer:
|
||||
self.enable_cors = enable_cors
|
||||
self.frontend_src = frontend_src
|
||||
self.favicon_path: Path | str = favicon_path # type: ignore
|
||||
self.enable_frontend = enable_frontend
|
||||
|
||||
if self.favicon_path is None:
|
||||
self.favicon_path = self.frontend_src / "favicon.ico"
|
||||
|
||||
self._service_config_dir = config_dir
|
||||
self._generate_web_settings = generate_web_settings
|
||||
self._loop: asyncio.AbstractEventLoop
|
||||
self._loop = asyncio.get_event_loop()
|
||||
self._sio = setup_sio_server(self.observer, self.enable_cors, self._loop)
|
||||
self._initialise_configuration()
|
||||
|
||||
async def serve(self) -> None:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._sio = setup_sio_server(self.observer, self.enable_cors, self._loop)
|
||||
|
||||
async def index(
|
||||
request: aiohttp.web.Request,
|
||||
) -> aiohttp.web.Response | aiohttp.web.FileResponse:
|
||||
@@ -162,15 +162,17 @@ class WebServer:
|
||||
|
||||
# Define routes
|
||||
self._sio.attach(app, socketio_path="/ws/socket.io")
|
||||
app.router.add_static("/assets", self.frontend_src / "assets")
|
||||
app.router.add_get("/favicon.ico", self._favicon_route)
|
||||
app.router.add_get("/service-properties", self._service_properties_route)
|
||||
app.router.add_get("/web-settings", self._web_settings_route)
|
||||
app.router.add_get("/custom.css", self._styles_route)
|
||||
if self.enable_frontend:
|
||||
app.router.add_static("/assets", self.frontend_src / "assets")
|
||||
app.router.add_get("/favicon.ico", self._favicon_route)
|
||||
app.router.add_get("/service-properties", self._service_properties_route)
|
||||
app.router.add_get("/web-settings", self._web_settings_route)
|
||||
app.router.add_get("/custom.css", self._styles_route)
|
||||
app.add_subapp("/api/", create_api_application(self.state_manager))
|
||||
|
||||
app.router.add_get(r"/", index)
|
||||
app.router.add_get(r"/{tail:.*}", index)
|
||||
if self.enable_frontend:
|
||||
app.router.add_get(r"/", index)
|
||||
app.router.add_get(r"/{tail:.*}", index)
|
||||
|
||||
await aiohttp.web._run_app(
|
||||
app,
|
||||
|
||||
@@ -165,15 +165,16 @@ class SocketIOHandler(logging.Handler):
|
||||
log_entry = self.format(record)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.create_task(
|
||||
self._sio.emit(
|
||||
"log",
|
||||
{
|
||||
"levelname": record.levelname,
|
||||
"message": log_entry,
|
||||
},
|
||||
if loop.is_running():
|
||||
loop.create_task(
|
||||
self._sio.emit(
|
||||
"log",
|
||||
{
|
||||
"levelname": record.levelname,
|
||||
"message": log_entry,
|
||||
},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def setup_logging() -> None:
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import sys
|
||||
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
import pydase
|
||||
import pydase.components
|
||||
from pydase.data_service.data_service_observer import DataServiceObserver
|
||||
from pydase.data_service.state_manager import StateManager
|
||||
from pydase.utils.serialization.serializer import dump
|
||||
from pytest import LogCaptureFixture
|
||||
|
||||
if sys.version_info < (3, 13):
|
||||
PATHLIB_PATH = "pathlib.Path"
|
||||
else:
|
||||
PATHLIB_PATH = "pathlib._local.Path"
|
||||
|
||||
|
||||
def test_image_functions(caplog: LogCaptureFixture) -> None:
|
||||
@@ -106,7 +113,7 @@ def test_image_serialization() -> None:
|
||||
"signature": {
|
||||
"parameters": {
|
||||
"path": {
|
||||
"annotation": "pathlib.Path | str",
|
||||
"annotation": f"{PATHLIB_PATH} | str",
|
||||
"default": {},
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user