mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-05 21:20:40 +02:00
Compare commits
22 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 |
@ -58,7 +58,7 @@ 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
|
||||
@ -77,7 +77,7 @@ 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.
|
||||
- By setting `client_id`, the server will log a descriptive identifier for this client via the `X-Client-Id` HTTP header.
|
||||
- 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
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "pydase"
|
||||
version = "0.10.13"
|
||||
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 = [
|
||||
{name = "Mose Müller",email = "mosemueller@gmail.com"}
|
||||
|
@ -1,13 +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 aiohttp_socks.connector
|
||||
import socketio # type: ignore
|
||||
|
||||
from pydase.client.proxy_class import ProxyClass
|
||||
@ -59,7 +60,8 @@ class Client:
|
||||
client's behaviour (e.g., reconnection attempts or reconnection delay).
|
||||
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.
|
||||
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`).
|
||||
@ -112,7 +114,7 @@ class Client:
|
||||
self._path_prefix = parsed_url.path.rstrip("/") # Remove trailing slash if any
|
||||
self._url = url
|
||||
self._proxy_url = proxy_url
|
||||
self._client_id = client_id
|
||||
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
|
||||
@ -150,6 +152,17 @@ class Client:
|
||||
|
||||
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
|
||||
|
@ -20,6 +20,19 @@ 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
|
||||
|
||||
|
@ -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:
|
||||
"""
|
||||
|
@ -231,6 +231,6 @@ def current_event_loop_exists() -> bool:
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
return not asyncio.get_running_loop().is_closed()
|
||||
return not asyncio.get_event_loop().is_closed()
|
||||
except RuntimeError:
|
||||
return False
|
||||
|
@ -168,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")
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user