mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-19 04:31:19 +01:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5702adbdbd | ||
|
|
e3a7932ac4 | ||
|
|
21cd039610 | ||
|
|
0b5dd5393a | ||
|
|
0f7a8ec63b | ||
|
|
6fe30fc6ec | ||
|
|
5f64ec131c | ||
|
|
06bf5fb539 | ||
|
|
bdbb79e131 | ||
|
|
78b94d4fc8 | ||
|
|
6d12371e7b | ||
|
|
fb6146e01d |
46
.github/workflows/publish-to-pypi.yaml
vendored
46
.github/workflows/publish-to-pypi.yaml
vendored
@@ -86,26 +86,26 @@ jobs:
|
|||||||
'${{ github.ref_name }}' dist/**
|
'${{ github.ref_name }}' dist/**
|
||||||
--repo '${{ github.repository }}'
|
--repo '${{ github.repository }}'
|
||||||
|
|
||||||
publish-to-testpypi:
|
# publish-to-testpypi:
|
||||||
name: Publish Python 🐍 distribution 📦 to TestPyPI
|
# name: Publish Python 🐍 distribution 📦 to TestPyPI
|
||||||
needs:
|
# needs:
|
||||||
- build
|
# - build
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
|
#
|
||||||
environment:
|
# environment:
|
||||||
name: testpypi
|
# name: testpypi
|
||||||
url: https://test.pypi.org/p/pydase
|
# url: https://test.pypi.org/p/pydase
|
||||||
|
#
|
||||||
permissions:
|
# permissions:
|
||||||
id-token: write # IMPORTANT: mandatory for trusted publishing
|
# id-token: write # IMPORTANT: mandatory for trusted publishing
|
||||||
|
#
|
||||||
steps:
|
# steps:
|
||||||
- name: Download all the dists
|
# - name: Download all the dists
|
||||||
uses: actions/download-artifact@v3
|
# uses: actions/download-artifact@v3
|
||||||
with:
|
# with:
|
||||||
name: python-package-distributions
|
# name: python-package-distributions
|
||||||
path: dist/
|
# path: dist/
|
||||||
- name: Publish distribution 📦 to TestPyPI
|
# - name: Publish distribution 📦 to TestPyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
# uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
with:
|
# with:
|
||||||
repository-url: https://test.pypi.org/legacy/
|
# repository-url: https://test.pypi.org/legacy/
|
||||||
|
|||||||
0
.python-version
Normal file
0
.python-version
Normal file
13
README.md
13
README.md
@@ -44,13 +44,13 @@
|
|||||||
Install pydase using [`poetry`](https://python-poetry.org/):
|
Install pydase using [`poetry`](https://python-poetry.org/):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
poetry add git+https://github.com/tiqi-group/pydase.git
|
poetry add pydase
|
||||||
```
|
```
|
||||||
|
|
||||||
or `pip`:
|
or `pip`:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install git+https://github.com/tiqi-group/pydase.git
|
pip install pydase
|
||||||
```
|
```
|
||||||
<!--installation-end-->
|
<!--installation-end-->
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ Below are the components available in the `pydase.components` module, accompanie
|
|||||||
|
|
||||||
#### Extending with New Components
|
#### Extending with New Components
|
||||||
|
|
||||||
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](./docs/dev-guide/Adding_Components.md) provides detailed steps on achieving this.
|
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](https://pydase.readthedocs.io/en/latest/dev-guide/Adding_Components/) provides detailed steps on achieving this.
|
||||||
|
|
||||||
## Understanding Service Persistence
|
## Understanding Service Persistence
|
||||||
|
|
||||||
@@ -422,6 +422,7 @@ For more information about what you can do with the units, please consult the do
|
|||||||
## Changing the Log Level
|
## Changing the Log Level
|
||||||
|
|
||||||
You can change the log level of loguru by either
|
You can change the log level of loguru 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
|
||||||
@@ -442,12 +443,12 @@ You can change the log level of loguru by either
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](URL_TO_YOUR_DOCUMENTATION) for more information.
|
The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](https://pydase.readthedocs.io/en/latest/) for more information.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
We welcome contributions! Please see [contributing.md](./docs/about/contributing.md) for details on how to contribute.
|
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/latest/about/contributing/) for details on how to contribute.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
`pydase` is licensed under the [MIT License](./LICENSE).
|
`pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "pydase"
|
name = "pydase"
|
||||||
version = "0.1.0"
|
version = "0.1.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"
|
||||||
|
|||||||
@@ -83,8 +83,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
|
|
||||||
def __check_instance_classes(self) -> None:
|
def __check_instance_classes(self) -> None:
|
||||||
for attr_name, attr_value in get_class_and_instance_attributes(self).items():
|
for attr_name, attr_value in get_class_and_instance_attributes(self).items():
|
||||||
# every class defined by the user should inherit from DataService
|
# every class defined by the user should inherit from DataService if it is
|
||||||
if not attr_name.startswith("_DataService__"):
|
# assigned to a public attribute
|
||||||
|
if not attr_name.startswith("_"):
|
||||||
warn_if_instance_class_does_not_inherit_from_DataService(attr_value)
|
warn_if_instance_class_does_not_inherit_from_DataService(attr_value)
|
||||||
|
|
||||||
def __set_attribute_based_on_type( # noqa:CFQ002
|
def __set_attribute_based_on_type( # noqa:CFQ002
|
||||||
@@ -198,22 +199,10 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
value, readonly status, and documentation if any in the resulting dictionary.
|
value, readonly status, and documentation if any in the resulting dictionary.
|
||||||
Attributes and methods starting with an underscore are ignored.
|
Attributes and methods starting with an underscore are ignored.
|
||||||
|
|
||||||
For attributes, methods, and properties unique to the class (not inherited from
|
For nested DataService instances, the method serializes recursively.
|
||||||
the base class), the method uses the format "<prefix>.<key>" for keys in the
|
|
||||||
dictionary. If no prefix is provided, the key format is simply "<key>".
|
|
||||||
|
|
||||||
For nested DataService instances, the method serializes recursively and appends
|
|
||||||
the key of the nested instance to the prefix in the format "<prefix>.<key>".
|
|
||||||
|
|
||||||
For attributes of type list, each item in the list is serialized individually.
|
For attributes of type list, each item in the list is serialized individually.
|
||||||
If an item in the list is an instance of DataService, it is serialized
|
If an item in the list is an instance of DataService, it is serialized
|
||||||
recursively with its key in the format "<prefix>.<key>.<item_id>", where
|
recursively.
|
||||||
"item_id" is the id of the item itself.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
prefix (str, optional): The prefix for each key in the serialized
|
|
||||||
dictionary. This is mainly used when this method is called recursively to
|
|
||||||
maintain the structure of nested instances.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: The serialized instance.
|
dict: The serialized instance.
|
||||||
@@ -321,16 +310,6 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
"readonly": True,
|
"readonly": True,
|
||||||
"value": running_task_info,
|
"value": running_task_info,
|
||||||
}
|
}
|
||||||
elif isinstance(getattr(self.__class__, key, None), property):
|
|
||||||
prop: property = getattr(self.__class__, key)
|
|
||||||
result[key] = {
|
|
||||||
"type": type(value).__name__,
|
|
||||||
"value": value
|
|
||||||
if not isinstance(value, u.Quantity)
|
|
||||||
else {"magnitude": value.m, "unit": str(value.u)},
|
|
||||||
"readonly": prop.fset is None,
|
|
||||||
"doc": get_attribute_doc(prop),
|
|
||||||
}
|
|
||||||
elif isinstance(value, Enum):
|
elif isinstance(value, Enum):
|
||||||
result[key] = {
|
result[key] = {
|
||||||
"type": "Enum",
|
"type": "Enum",
|
||||||
@@ -352,6 +331,11 @@ class DataService(rpyc.Service, AbstractDataService):
|
|||||||
"doc": get_attribute_doc(value),
|
"doc": get_attribute_doc(value),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isinstance(getattr(self.__class__, key, None), property):
|
||||||
|
prop: property = getattr(self.__class__, key)
|
||||||
|
result[key]["readonly"] = prop.fset is None
|
||||||
|
result[key]["doc"] = get_attribute_doc(prop)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def update_DataService_attribute(
|
def update_DataService_attribute(
|
||||||
|
|||||||
@@ -28,9 +28,8 @@ class AdditionalServerProtocol(Protocol):
|
|||||||
|
|
||||||
This protocol sets the standard for how additional servers should be implemented
|
This protocol sets the standard for how additional servers should be implemented
|
||||||
to ensure compatibility with the main Server class. The protocol requires that
|
to ensure compatibility with the main Server class. The protocol requires that
|
||||||
any server implementing it should have an __init__ method for initialization, a
|
any server implementing it should have an __init__ method for initialization and a
|
||||||
serve method for starting the server, and an install_signal_handlers method for
|
serve method for starting the server.
|
||||||
setting up signal handlers.
|
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
-----------
|
-----------
|
||||||
@@ -62,12 +61,6 @@ class AdditionalServerProtocol(Protocol):
|
|||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
def install_signal_handlers(self) -> None:
|
|
||||||
"""Sets up signal handlers for the server. This method is used to define how the
|
|
||||||
server should respond to various system signals, such as SIGINT and SIGTERM.
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
class AdditionalServer(TypedDict):
|
class AdditionalServer(TypedDict):
|
||||||
"""
|
"""
|
||||||
@@ -257,13 +250,6 @@ class Server:
|
|||||||
info=self._info,
|
info=self._info,
|
||||||
**server["kwargs"],
|
**server["kwargs"],
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
addin_server.install_signal_handlers = lambda: None # type: ignore
|
|
||||||
except Exception:
|
|
||||||
logger.debug(
|
|
||||||
"Additional server does not have a method called "
|
|
||||||
"'install_signal_handlers'."
|
|
||||||
)
|
|
||||||
|
|
||||||
server_name = (
|
server_name = (
|
||||||
addin_server.__module__ + "." + addin_server.__class__.__name__
|
addin_server.__module__ + "." + addin_server.__class__.__name__
|
||||||
|
|||||||
64
tests/data_service/test_data_service.py
Normal file
64
tests/data_service/test_data_service.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
import pydase
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_serialize() -> None:
|
||||||
|
class EnumClass(Enum):
|
||||||
|
FOO = "foo"
|
||||||
|
BAR = "bar"
|
||||||
|
|
||||||
|
class EnumAttribute(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.some_enum = EnumClass.FOO
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
class EnumPropertyWithoutSetter(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._some_enum = EnumClass.FOO
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def some_enum(self) -> EnumClass:
|
||||||
|
return self._some_enum
|
||||||
|
|
||||||
|
class EnumPropertyWithSetter(pydase.DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._some_enum = EnumClass.FOO
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def some_enum(self) -> EnumClass:
|
||||||
|
return self._some_enum
|
||||||
|
|
||||||
|
@some_enum.setter
|
||||||
|
def some_enum(self, value: EnumClass) -> None:
|
||||||
|
self._some_enum = value
|
||||||
|
|
||||||
|
assert EnumAttribute().serialize() == {
|
||||||
|
"some_enum": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": "FOO",
|
||||||
|
"enum": {"FOO": "foo", "BAR": "bar"},
|
||||||
|
"readonly": False,
|
||||||
|
"doc": None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert EnumPropertyWithoutSetter().serialize() == {
|
||||||
|
"some_enum": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": "FOO",
|
||||||
|
"enum": {"FOO": "foo", "BAR": "bar"},
|
||||||
|
"readonly": True,
|
||||||
|
"doc": None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert EnumPropertyWithSetter().serialize() == {
|
||||||
|
"some_enum": {
|
||||||
|
"type": "Enum",
|
||||||
|
"value": "FOO",
|
||||||
|
"enum": {"FOO": "foo", "BAR": "bar"},
|
||||||
|
"readonly": False,
|
||||||
|
"doc": None,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ from pytest import LogCaptureFixture
|
|||||||
|
|
||||||
from pydase import DataService
|
from pydase import DataService
|
||||||
|
|
||||||
from . import caplog # noqa
|
from .. import caplog # noqa
|
||||||
|
|
||||||
|
|
||||||
def test_setattr_warnings(caplog: LogCaptureFixture) -> None: # noqa
|
def test_setattr_warnings(caplog: LogCaptureFixture) -> None: # noqa
|
||||||
@@ -32,3 +32,19 @@ def test_private_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
|
|||||||
" Warning: You should not set private but rather protected attributes! Use "
|
" Warning: You should not set private but rather protected attributes! Use "
|
||||||
"_something instead of __something." in caplog.text
|
"_something instead of __something." in caplog.text
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_protected_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
|
||||||
|
class SubClass:
|
||||||
|
name = "Hello"
|
||||||
|
|
||||||
|
class ServiceClass(DataService):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._subclass = SubClass
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
ServiceClass()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
"Warning: Class SubClass does not inherit from DataService." not in caplog.text
|
||||||
|
)
|
||||||
Reference in New Issue
Block a user