mirror of
https://github.com/tiqi-group/pydase.git
synced 2026-02-14 06:18:41 +01:00
feat: adding support for units
With `pint` as the unit package, the user can now define Quantities as attributes of their DataService class. This will be rendered as a float in the frontend application with the unit as an additional field appended to the form.
This commit is contained in:
@@ -8,6 +8,7 @@ from typing import Any, Optional, cast, get_type_hints
|
||||
import rpyc
|
||||
from loguru import logger
|
||||
|
||||
import pydase.units as u
|
||||
from pydase.utils.helpers import (
|
||||
convert_arguments_to_hinted_types,
|
||||
generate_paths_from_DataService_dict,
|
||||
@@ -62,6 +63,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
if isinstance(current_value, float) and isinstance(__value, int):
|
||||
__value = float(__value)
|
||||
|
||||
if isinstance(current_value, u.Quantity):
|
||||
__value = u.convert_to_quantity(__value, str(current_value.u))
|
||||
|
||||
super().__setattr__(__name, __value)
|
||||
|
||||
if self.__dict__.get("_initialised") and not __name == "_initialised":
|
||||
@@ -263,7 +267,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
prop: property = getattr(self.__class__, key)
|
||||
result[key] = {
|
||||
"type": type(value).__name__,
|
||||
"value": value,
|
||||
"value": value
|
||||
if not isinstance(value, u.Quantity)
|
||||
else {"magnitude": value.m, "unit": str(value.u)},
|
||||
"readonly": prop.fset is None,
|
||||
"doc": inspect.getdoc(prop),
|
||||
}
|
||||
@@ -279,7 +285,9 @@ class DataService(rpyc.Service, AbstractDataService):
|
||||
else:
|
||||
result[key] = {
|
||||
"type": type(value).__name__,
|
||||
"value": value,
|
||||
"value": value
|
||||
if not isinstance(value, u.Quantity)
|
||||
else {"magnitude": value.m, "unit": str(value.u)},
|
||||
"readonly": False,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.d5ec2545.css",
|
||||
"main.js": "/static/js/main.b51434ec.js",
|
||||
"main.js": "/static/js/main.09e2d82a.js",
|
||||
"index.html": "/index.html",
|
||||
"main.d5ec2545.css.map": "/static/css/main.d5ec2545.css.map",
|
||||
"main.b51434ec.js.map": "/static/js/main.b51434ec.js.map"
|
||||
"main.09e2d82a.js.map": "/static/js/main.09e2d82a.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.d5ec2545.css",
|
||||
"static/js/main.b51434ec.js"
|
||||
"static/js/main.09e2d82a.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.b51434ec.js"></script><link href="/static/css/main.d5ec2545.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.09e2d82a.js"></script><link href="/static/css/main.d5ec2545.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
3
src/pydase/frontend/static/js/main.09e2d82a.js
Normal file
3
src/pydase/frontend/static/js/main.09e2d82a.js
Normal file
File diff suppressed because one or more lines are too long
1
src/pydase/frontend/static/js/main.09e2d82a.js.map
Normal file
1
src/pydase/frontend/static/js/main.09e2d82a.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@ from rpyc import (
|
||||
from rpyc import ThreadedServer
|
||||
from uvicorn.server import HANDLED_SIGNALS
|
||||
|
||||
import pydase.units as u
|
||||
from pydase import DataService
|
||||
from pydase.version import __version__
|
||||
|
||||
@@ -295,6 +296,12 @@ class Server:
|
||||
# > File "/usr/lib64/python3.11/json/encoder.py", line 180, in default
|
||||
# > raise TypeError(f'Object of type {o.__class__.__name__} '
|
||||
# > TypeError: Object of type list is not JSON serializable
|
||||
notify_value = value
|
||||
if isinstance(value, Enum):
|
||||
notify_value = value.name
|
||||
if isinstance(value, u.Quantity):
|
||||
notify_value = {"magnitude": value.m, "unit": str(value.u)}
|
||||
|
||||
async def notify() -> None:
|
||||
try:
|
||||
await self._wapi.sio.emit( # type: ignore
|
||||
@@ -303,11 +310,7 @@ class Server:
|
||||
"data": {
|
||||
"parent_path": parent_path,
|
||||
"name": name,
|
||||
"value": value.name
|
||||
if isinstance(
|
||||
value, Enum
|
||||
) # enums are not JSON serializable
|
||||
else value,
|
||||
"value": notify_value,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
55
src/pydase/units.py
Normal file
55
src/pydase/units.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from typing import TypedDict
|
||||
|
||||
import pint
|
||||
|
||||
units: pint.UnitRegistry = pint.UnitRegistry()
|
||||
units.default_format = "~P" # pretty and short format
|
||||
|
||||
Quantity = pint.Quantity
|
||||
Unit = units.Unit
|
||||
|
||||
|
||||
class QuantityDict(TypedDict):
|
||||
magnitude: int | float
|
||||
unit: str
|
||||
|
||||
|
||||
def convert_to_quantity(
|
||||
value: QuantityDict | float | int | Quantity, unit: str = ""
|
||||
) -> Quantity:
|
||||
"""
|
||||
Convert a given value into a pint.Quantity object with the specified unit.
|
||||
|
||||
Args:
|
||||
value (QuantityDict | float | int | Quantity):
|
||||
The value to be converted into a Quantity object.
|
||||
- If value is a float or int, it will be directly converted to the specified
|
||||
unit.
|
||||
- If value is a dict, it must have keys 'magnitude' and 'unit' to represent
|
||||
the value and unit.
|
||||
- If value is a Quantity object, it will remain unchanged.\n
|
||||
unit (str, optional): The target unit for conversion. If empty and value is not
|
||||
a Quantity object, it will assume a unitless quantity.
|
||||
|
||||
Returns:
|
||||
Quantity: The converted value as a pint.Quantity object with the specified unit.
|
||||
|
||||
Examples:
|
||||
>>> convert_to_quantity(5, 'm')
|
||||
<Quantity(5.0, 'meters')>
|
||||
>>> convert_to_quantity({'magnitude': 10, 'unit': 'mV'})
|
||||
<Quantity(10.0, 'millivolt')>
|
||||
>>> convert_to_quantity(10.0 * u.units.V)
|
||||
<Quantity(10.0, 'volt')>
|
||||
|
||||
Notes:
|
||||
- If unit is not provided and value is a float or int, the resulting Quantity will be unitless.
|
||||
"""
|
||||
|
||||
if isinstance(value, int | float):
|
||||
quantity = float(value) * Unit(unit)
|
||||
elif isinstance(value, dict):
|
||||
quantity = float(value["magnitude"]) * Unit(value["unit"])
|
||||
else:
|
||||
quantity = value
|
||||
return quantity # type: ignore
|
||||
@@ -106,7 +106,7 @@ def generate_paths_from_DataService_dict(
|
||||
# ignoring methods
|
||||
continue
|
||||
new_path = f"{parent_path}.{key}" if parent_path else key
|
||||
if isinstance(value["value"], dict):
|
||||
if isinstance(value["value"], dict) and value["type"] != "Quantity":
|
||||
paths.extend(generate_paths_from_DataService_dict(value["value"], new_path)) # type: ignore
|
||||
elif isinstance(value["value"], list):
|
||||
for index, item in enumerate(value["value"]):
|
||||
@@ -124,7 +124,7 @@ def generate_paths_from_DataService_dict(
|
||||
return paths
|
||||
|
||||
|
||||
STANDARD_TYPES = ("int", "float", "bool", "str", "Enum", "NoneType")
|
||||
STANDARD_TYPES = ("int", "float", "bool", "str", "Enum", "NoneType", "Quantity")
|
||||
|
||||
|
||||
def get_nested_value_by_path_and_key(data: dict, path: str, key: str = "value") -> Any:
|
||||
|
||||
@@ -14,7 +14,7 @@ def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) ->
|
||||
"_abc",
|
||||
]
|
||||
and base_class_name not in ["DataService", "list", "Enum"]
|
||||
and type(__value).__name__ not in ["CallbackManager", "TaskManager"]
|
||||
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
|
||||
):
|
||||
logger.warning(
|
||||
f"Warning: Class {type(__value).__name__} does not inherit from DataService."
|
||||
|
||||
Reference in New Issue
Block a user