mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-19 12:41:19 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bbf479a440 | ||
|
|
983d392ba8 | ||
|
|
56dd9dd8aa | ||
|
|
20028c379d | ||
|
|
e48046795e | ||
|
|
1ac9e45c73 | ||
|
|
488415436c | ||
|
|
d7c5c2cd6e | ||
|
|
5388fd0d2b | ||
|
|
e74b5c773a | ||
|
|
bb6cd159f1 | ||
|
|
4a09f02882 |
13
README.md
13
README.md
@@ -105,7 +105,7 @@ class Device(pydase.DataService):
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service = Device()
|
service = Device()
|
||||||
pydase.Server(service=service).run()
|
pydase.Server(service=service, web_port=8001).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
In the above example, we define a `Device` class that inherits from `pydase.DataService`.
|
In the above example, we define a `Device` class that inherits from `pydase.DataService`.
|
||||||
@@ -122,10 +122,13 @@ import pydase
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
service = Device()
|
service = Device()
|
||||||
pydase.Server(service=service).run()
|
pydase.Server(service=service, web_port=8001).run()
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the server, making your `Device` service accessible on [http://localhost:8001](http://localhost:8001).
|
This will start the server, making your `Device` service accessible on
|
||||||
|
[http://localhost:8001](http://localhost:8001). The port number for the web server can
|
||||||
|
be customised in the server constructor or through environment variables and defaults
|
||||||
|
to `8001`.
|
||||||
|
|
||||||
### Accessing the Web Interface
|
### Accessing the Web Interface
|
||||||
|
|
||||||
@@ -144,7 +147,7 @@ import pydase
|
|||||||
|
|
||||||
# Replace the hostname and port with the IP address and the port of the machine where
|
# Replace the hostname and port with the IP address and the port of the machine where
|
||||||
# the service is running, respectively
|
# the service is running, respectively
|
||||||
client_proxy = pydase.Client(url="ws://<ip_addr>:<service_port>").proxy
|
client_proxy = pydase.Client(url="ws://<ip_addr>:<web_port>").proxy
|
||||||
# client_proxy = pydase.Client(url="wss://your-domain.ch").proxy # if your service uses ssl-encryption
|
# client_proxy = pydase.Client(url="wss://your-domain.ch").proxy # if your service uses ssl-encryption
|
||||||
|
|
||||||
# After the connection, interact with the service attributes as if they were local
|
# After the connection, interact with the service attributes as if they were local
|
||||||
@@ -170,7 +173,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
"http://<hostname>:<port>/api/v1/get_value?access_path=<full_access_path>"
|
"http://<hostname>:<web_port>/api/v1/get_value?access_path=<full_access_path>"
|
||||||
)
|
)
|
||||||
serialized_value = json.loads(response.text)
|
serialized_value = json.loads(response.text)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -132,6 +132,8 @@ const handleNumericKey = (
|
|||||||
selectionStart: number,
|
selectionStart: number,
|
||||||
selectionEnd: number,
|
selectionEnd: number,
|
||||||
) => {
|
) => {
|
||||||
|
let newValue = value;
|
||||||
|
|
||||||
// Check if a number key or a decimal point key is pressed
|
// Check if a number key or a decimal point key is pressed
|
||||||
if (key === "." && value.includes(".")) {
|
if (key === "." && value.includes(".")) {
|
||||||
// Check if value already contains a decimal. If so, ignore input.
|
// Check if value already contains a decimal. If so, ignore input.
|
||||||
@@ -139,14 +141,34 @@ const handleNumericKey = (
|
|||||||
return { value, selectionStart };
|
return { value, selectionStart };
|
||||||
}
|
}
|
||||||
|
|
||||||
let newValue = value;
|
// Handle minus sign input
|
||||||
|
if (key === "-") {
|
||||||
|
if (selectionStart === 0 && selectionEnd > selectionStart) {
|
||||||
|
// Replace selection with minus if selection starts at 0
|
||||||
|
newValue = "-" + value.slice(selectionEnd);
|
||||||
|
selectionStart = 1;
|
||||||
|
} else if (selectionStart === 0 && !value.startsWith("-")) {
|
||||||
|
// Add minus at the beginning if it doesn't exist
|
||||||
|
newValue = "-" + value;
|
||||||
|
selectionStart = 1;
|
||||||
|
} else if (
|
||||||
|
(selectionStart === 0 || selectionStart === 1) &&
|
||||||
|
value.startsWith("-")
|
||||||
|
) {
|
||||||
|
// Remove minus if it exists
|
||||||
|
newValue = value.slice(1);
|
||||||
|
selectionStart = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: newValue, selectionStart };
|
||||||
|
}
|
||||||
|
|
||||||
// Add the new key at the cursor's position
|
// Add the new key at the cursor's position
|
||||||
if (selectionEnd > selectionStart) {
|
if (selectionEnd > selectionStart) {
|
||||||
// If there is a selection, replace it with the key
|
// If there is a selection, replace it with the key
|
||||||
newValue = value.slice(0, selectionStart) + key + value.slice(selectionEnd);
|
newValue = value.slice(0, selectionStart) + key + value.slice(selectionEnd);
|
||||||
} else {
|
} else {
|
||||||
// otherwise, append the key after the selection start
|
// Otherwise, insert the key at the cursor position
|
||||||
newValue = value.slice(0, selectionStart) + key + value.slice(selectionStart);
|
newValue = value.slice(0, selectionStart) + key + value.slice(selectionStart);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,17 +223,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
|||||||
// Select everything when pressing Ctrl + a
|
// Select everything when pressing Ctrl + a
|
||||||
inputTarget.setSelectionRange(0, value.length);
|
inputTarget.setSelectionRange(0, value.length);
|
||||||
return;
|
return;
|
||||||
} else if (key === "-") {
|
} else if ((key >= "0" && key <= "9") || key === "-") {
|
||||||
if (selectionStart === 0 && !value.startsWith("-")) {
|
|
||||||
newValue = "-" + value;
|
|
||||||
selectionStart++;
|
|
||||||
} else if (value.startsWith("-") && selectionStart === 1) {
|
|
||||||
newValue = value.substring(1); // remove minus sign
|
|
||||||
selectionStart--;
|
|
||||||
} else {
|
|
||||||
return; // Ignore "-" pressed in other positions
|
|
||||||
}
|
|
||||||
} else if (key >= "0" && key <= "9") {
|
|
||||||
// Check if a number key or a decimal point key is pressed
|
// Check if a number key or a decimal point key is pressed
|
||||||
({ value: newValue, selectionStart } = handleNumericKey(
|
({ value: newValue, selectionStart } = handleNumericKey(
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pydase"
|
name = "pydase"
|
||||||
version = "0.10.0"
|
version = "0.10.2"
|
||||||
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."
|
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 = ["Mose Mueller <mosmuell@ethz.ch>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="description" content="Web site displaying a pydase UI." />
|
<meta name="description" content="Web site displaying a pydase UI." />
|
||||||
<script type="module" crossorigin src="/assets/index-DI9re3au.js"></script>
|
<script type="module" crossorigin src="/assets/index-BjsjosWf.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-D2aktF3W.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-D2aktF3W.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -17,16 +17,18 @@ def autostart_service_tasks(
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
for attr in dir(service):
|
for attr in dir(service):
|
||||||
if is_property_attribute(service, attr): # prevent eval of property attrs
|
if is_property_attribute(service, attr) or attr in {
|
||||||
|
"_observers",
|
||||||
|
"__dict__",
|
||||||
|
}: # prevent eval of property attrs and recursion
|
||||||
continue
|
continue
|
||||||
|
|
||||||
val = getattr(service, attr)
|
val = getattr(service, attr)
|
||||||
if (
|
if isinstance(val, pydase.task.task.Task):
|
||||||
isinstance(val, pydase.task.task.Task)
|
if val.autostart and val.status == TaskStatus.NOT_RUNNING:
|
||||||
and val.autostart
|
val.start()
|
||||||
and val.status == TaskStatus.NOT_RUNNING
|
else:
|
||||||
):
|
continue
|
||||||
val.start()
|
|
||||||
else:
|
else:
|
||||||
autostart_nested_service_tasks(val)
|
autostart_nested_service_tasks(val)
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ def autostart_nested_service_tasks(
|
|||||||
autostart_service_tasks(service)
|
autostart_service_tasks(service)
|
||||||
elif isinstance(service, list):
|
elif isinstance(service, list):
|
||||||
for entry in service:
|
for entry in service:
|
||||||
autostart_service_tasks(entry)
|
autostart_nested_service_tasks(entry)
|
||||||
elif isinstance(service, dict):
|
elif isinstance(service, dict):
|
||||||
for entry in service.values():
|
for entry in service.values():
|
||||||
autostart_service_tasks(entry)
|
autostart_nested_service_tasks(entry)
|
||||||
|
|||||||
@@ -18,23 +18,30 @@ async def test_start_and_stop_task(caplog: LogCaptureFixture) -> None:
|
|||||||
class MyService(pydase.DataService):
|
class MyService(pydase.DataService):
|
||||||
@task()
|
@task()
|
||||||
async def my_task(self) -> None:
|
async def my_task(self) -> None:
|
||||||
|
logger.info("Triggered task.")
|
||||||
while True:
|
while True:
|
||||||
logger.debug("Logging message")
|
await asyncio.sleep(1)
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
|
|
||||||
# Your test code here
|
# Your test code here
|
||||||
service_instance = MyService()
|
service_instance = MyService()
|
||||||
state_manager = StateManager(service_instance)
|
state_manager = StateManager(service_instance)
|
||||||
DataServiceObserver(state_manager)
|
DataServiceObserver(state_manager)
|
||||||
|
|
||||||
|
autostart_service_tasks(service_instance)
|
||||||
|
await asyncio.sleep(0.1)
|
||||||
|
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
|
||||||
|
|
||||||
service_instance.my_task.start()
|
service_instance.my_task.start()
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
assert service_instance.my_task.status == TaskStatus.RUNNING
|
||||||
|
|
||||||
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
|
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
|
||||||
assert "Logging message" in caplog.text
|
assert "Triggered task." in caplog.text
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
service_instance.my_task.stop()
|
service_instance.my_task.stop()
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
assert service_instance.my_task.status == TaskStatus.NOT_RUNNING
|
||||||
assert "Task 'my_task' was cancelled" in caplog.text
|
assert "Task 'my_task' was cancelled" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +51,8 @@ async def test_autostart_task(caplog: LogCaptureFixture) -> None:
|
|||||||
@task(autostart=True)
|
@task(autostart=True)
|
||||||
async def my_task(self) -> None:
|
async def my_task(self) -> None:
|
||||||
logger.info("Triggered task.")
|
logger.info("Triggered task.")
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
# Your test code here
|
# Your test code here
|
||||||
service_instance = MyService()
|
service_instance = MyService()
|
||||||
@@ -53,6 +62,7 @@ async def test_autostart_task(caplog: LogCaptureFixture) -> None:
|
|||||||
autostart_service_tasks(service_instance)
|
autostart_service_tasks(service_instance)
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
assert service_instance.my_task.status == TaskStatus.RUNNING
|
||||||
|
|
||||||
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
|
assert "'my_task.status' changed to 'TaskStatus.RUNNING'" in caplog.text
|
||||||
|
|
||||||
@@ -65,6 +75,8 @@ async def test_nested_list_autostart_task(
|
|||||||
@task(autostart=True)
|
@task(autostart=True)
|
||||||
async def my_task(self) -> None:
|
async def my_task(self) -> None:
|
||||||
logger.info("Triggered task.")
|
logger.info("Triggered task.")
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
class MyService(pydase.DataService):
|
class MyService(pydase.DataService):
|
||||||
sub_services_list = [MySubService() for i in range(2)]
|
sub_services_list = [MySubService() for i in range(2)]
|
||||||
@@ -75,6 +87,8 @@ async def test_nested_list_autostart_task(
|
|||||||
autostart_service_tasks(service_instance)
|
autostart_service_tasks(service_instance)
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
assert service_instance.sub_services_list[0].my_task.status == TaskStatus.RUNNING
|
||||||
|
assert service_instance.sub_services_list[1].my_task.status == TaskStatus.RUNNING
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
"'sub_services_list[0].my_task.status' changed to 'TaskStatus.RUNNING'"
|
"'sub_services_list[0].my_task.status' changed to 'TaskStatus.RUNNING'"
|
||||||
@@ -111,6 +125,10 @@ async def test_nested_dict_autostart_task(
|
|||||||
assert (
|
assert (
|
||||||
service_instance.sub_services_dict["first"].my_task.status == TaskStatus.RUNNING
|
service_instance.sub_services_dict["first"].my_task.status == TaskStatus.RUNNING
|
||||||
)
|
)
|
||||||
|
assert (
|
||||||
|
service_instance.sub_services_dict["second"].my_task.status
|
||||||
|
== TaskStatus.RUNNING
|
||||||
|
)
|
||||||
|
|
||||||
assert (
|
assert (
|
||||||
"'sub_services_dict[\"first\"].my_task.status' changed to 'TaskStatus.RUNNING'"
|
"'sub_services_dict[\"first\"].my_task.status' changed to 'TaskStatus.RUNNING'"
|
||||||
|
|||||||
Reference in New Issue
Block a user