diff --git a/docs/dev-guide/api.md b/docs/dev-guide/api.md index e69de29..83ed19f 100644 --- a/docs/dev-guide/api.md +++ b/docs/dev-guide/api.md @@ -0,0 +1,39 @@ +::: pydase.data_service + handler: python + +::: pydase.server.server + handler: python + +::: pydase.server.web_server + handler: python + +::: pydase.client + handler: python + +::: pydase.components + handler: python + +::: pydase.utils.serialization.serializer + handler: python + +::: pydase.utils.serialization.deserializer + handler: python + options: + show_root_heading: true + show_root_toc_entry: false + show_symbol_type_heading: true + show_symbol_type_toc: true + +::: pydase.utils.serialization.types + handler: python + +::: pydase.utils.decorators + handler: python + options: + filters: ["!render_in_frontend"] + +::: pydase.units + handler: python + +::: pydase.config + handler: python diff --git a/docs/getting-started.md b/docs/getting-started.md index 938d812..e5b6bf0 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -5,7 +5,7 @@ end="" %} -[RESTful API]: ./user-guide/interaction/main.md#restful-api -[Python RPC Client]: ./user-guide/interaction/main.md#python-rpc-client +[RESTful API]: ./user-guide/interaction/README.md#restful-api +[Python RPC Client]: ./user-guide/interaction/README.md#python-rpc-client [Custom Components]: ./user-guide/Components.md#custom-components-pydasecomponents [Components]: ./user-guide/Components.md diff --git a/docs/index.md b/docs/index.md index 9b81dd3..50245c1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ [Defining DataService]: ./getting-started.md#defining-a-dataservice [Web Interface Access]: ./getting-started.md#accessing-the-web-interface [Short RPC Client]: ./getting-started.md#connecting-to-the-service-via-python-rpc-client -[Customizing Web Interface]: ./user-guide/interaction/main.md#customization-options +[Customizing Web Interface]: ./user-guide/interaction/README.md#customization-options [Task Management]: ./user-guide/Tasks.md [Units]: ./user-guide/Understanding-Units.md [Property Validation]: ./user-guide/Validating-Property-Setters.md diff --git a/docs/requirements.txt b/docs/requirements.txt index b274627..f41ac65 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -5,6 +5,7 @@ charset-normalizer==3.3.2 ; python_version >= "3.10" and python_version < "4.0" click==8.1.7 ; python_version >= "3.10" and python_version < "4.0" colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0" ghp-import==2.1.0 ; python_version >= "3.10" and python_version < "4.0" +griffe==1.1.0 ; python_version >= "3.10" and python_version < "4.0" idna==3.7 ; python_version >= "3.10" and python_version < "4.0" jinja2==3.1.4 ; python_version >= "3.10" and python_version < "4.0" markdown==3.6 ; python_version >= "3.10" and python_version < "4.0" @@ -14,10 +15,12 @@ mkdocs-autorefs==1.0.1 ; python_version >= "3.10" and python_version < "4.0" mkdocs-get-deps==0.2.0 ; python_version >= "3.10" and python_version < "4.0" mkdocs-include-markdown-plugin==3.9.1 ; python_version >= "3.10" and python_version < "4.0" mkdocs-material-extensions==1.3.1 ; python_version >= "3.10" and python_version < "4.0" -mkdocs-material==9.5.30 ; python_version >= "3.10" and python_version < "4.0" +mkdocs-material==9.5.31 ; python_version >= "3.10" and python_version < "4.0" mkdocs-swagger-ui-tag==0.6.10 ; python_version >= "3.10" and python_version < "4.0" mkdocs==1.6.0 ; python_version >= "3.10" and python_version < "4.0" -mkdocstrings==0.22.0 ; python_version >= "3.10" and python_version < "4.0" +mkdocstrings-python==1.10.8 ; python_version >= "3.10" and python_version < "4.0" +mkdocstrings==0.25.2 ; python_version >= "3.10" and python_version < "4.0" +mkdocstrings[python]==0.25.2 ; python_version >= "3.10" and python_version < "4.0" packaging==24.1 ; python_version >= "3.10" and python_version < "4.0" paginate==0.5.6 ; python_version >= "3.10" and python_version < "4.0" pathspec==0.12.1 ; python_version >= "3.10" and python_version < "4.0" diff --git a/docs/user-guide/Components.md b/docs/user-guide/Components.md index b1c74ec..43817cc 100644 --- a/docs/user-guide/Components.md +++ b/docs/user-guide/Components.md @@ -16,7 +16,7 @@ In `pydase`, components are fundamental building blocks that bridge the Python b ## Method Components Within the `DataService` class of `pydase`, only methods devoid of arguments can be represented in the frontend, classified into two distinct categories -1. [**Tasks**](#understanding-tasks-in-pydase): Argument-free asynchronous functions, identified within `pydase` as tasks, are inherently designed for frontend interaction. These tasks are automatically rendered with a start/stop button, allowing users to initiate or halt the task execution directly from the web interface. +1. [**Tasks**](./Tasks.md): Argument-free asynchronous functions, identified within `pydase` as tasks, are inherently designed for frontend interaction. These tasks are automatically rendered with a start/stop button, allowing users to initiate or halt the task execution directly from the web interface. 2. **Synchronous Methods with `@frontend` Decorator**: Synchronous methods without arguments can also be presented in the frontend. For this, they have to be decorated with the `@frontend` decorator. ```python @@ -348,7 +348,7 @@ In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` - Incorporating units in `NumberSlider` - The `NumberSlider` is capable of [displaying units](#understanding-units-in-pydase) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend. + The `NumberSlider` is capable of [displaying units](./Understanding-Units.md) alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend. Here's how to implement a `NumberSlider` with unit display: diff --git a/docs/user-guide/interaction/main.md b/docs/user-guide/interaction/README.md similarity index 100% rename from docs/user-guide/interaction/main.md rename to docs/user-guide/interaction/README.md diff --git a/mkdocs.yml b/mkdocs.yml index 84a6fbc..8ce6546 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -6,7 +6,7 @@ nav: - Getting Started: getting-started.md - User Guide: - Components Guide: user-guide/Components.md - - Interacting with pydase Services: user-guide/interaction/main.md + - Interacting with pydase Services: user-guide/interaction/README.md - Achieving Service Persistence: user-guide/Service_Persistence.md - Understanding Tasks: user-guide/Tasks.md - Understanding Units: user-guide/Understanding-Units.md @@ -44,10 +44,35 @@ markdown_extensions: plugins: - - include-markdown - - search - - mkdocstrings - - swagger-ui-tag +- include-markdown +- search +- mkdocstrings: + handlers: + python: + paths: [src] # search packages in the src folder + import: + - https://docs.python.org/3/objects.inv + - https://docs.pydantic.dev/latest/objects.inv + - https://confz.readthedocs.io/en/latest/objects.inv + options: + show_source: true + inherited_members: true + merge_init_into_class: true + show_signature_annotations: true + signature_crossrefs: true + separate_signature: true + docstring_options: + ignore_init_summary: true + # docstring_section_style: list + heading_level: 2 + parameter_headings: true + show_root_heading: true + show_root_full_path: true + show_symbol_type_heading: true + show_symbol_type_toc: true + # summary: true + unwrap_annotated: true +- swagger-ui-tag watch: - src/pydase diff --git a/poetry.lock b/poetry.lock index ac13a9a..d55cbd3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -769,6 +769,20 @@ python-dateutil = ">=2.8.1" [package.extras] dev = ["flake8", "markdown", "twine", "wheel"] +[[package]] +name = "griffe" +version = "1.1.0" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-1.1.0-py3-none-any.whl", hash = "sha256:38ccc5721571c95ae427123074cf0dc0d36bce7c9701ab2ada9fe0566ff50c10"}, + {file = "griffe-1.1.0.tar.gz", hash = "sha256:c6328cbdec0d449549c1cc332f59227cd5603f903479d73e4425d828b782ffc3"}, +] + +[package.dependencies] +colorama = ">=0.4" + [[package]] name = "h11" version = "0.14.0" @@ -1212,21 +1226,24 @@ beautifulsoup4 = ">=4.11.1" [[package]] name = "mkdocstrings" -version = "0.22.0" +version = "0.25.2" description = "Automatic documentation from sources, for MkDocs." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba"}, - {file = "mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256"}, + {file = "mkdocstrings-0.25.2-py3-none-any.whl", hash = "sha256:9e2cda5e2e12db8bb98d21e3410f3f27f8faab685a24b03b06ba7daa5b92abfc"}, + {file = "mkdocstrings-0.25.2.tar.gz", hash = "sha256:5cf57ad7f61e8be3111a2458b4e49c2029c9cb35525393b179f9c916ca8042dc"}, ] [package.dependencies] +click = ">=7.0" Jinja2 = ">=2.11.1" Markdown = ">=3.3" MarkupSafe = ">=1.1" -mkdocs = ">=1.2" +mkdocs = ">=1.4" mkdocs-autorefs = ">=0.3.1" +mkdocstrings-python = {version = ">=0.5.2", optional = true, markers = "extra == \"python\""} +platformdirs = ">=2.2.0" pymdown-extensions = ">=6.3" [package.extras] @@ -1234,6 +1251,21 @@ crystal = ["mkdocstrings-crystal (>=0.3.4)"] python = ["mkdocstrings-python (>=0.5.2)"] python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] +[[package]] +name = "mkdocstrings-python" +version = "1.10.8" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.10.8-py3-none-any.whl", hash = "sha256:bb12e76c8b071686617f824029cb1dfe0e9afe89f27fb3ad9a27f95f054dcd89"}, + {file = "mkdocstrings_python-1.10.8.tar.gz", hash = "sha256:5856a59cbebbb8deb133224a540de1ff60bded25e54d8beacc375bb133d39016"}, +] + +[package.dependencies] +griffe = ">=0.49" +mkdocstrings = ">=0.25" + [[package]] name = "multidict" version = "6.0.5" @@ -2464,4 +2496,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "54e25a68577a912301aa9125e3f3de545e03a199a79b2153c106285b92febbba" +content-hash = "7131eddc2065147a18c145bb6da09492f03eb7fe050e968109cecb6044d17ed6" diff --git a/pyproject.toml b/pyproject.toml index a881a33..ae2016f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ optional = true [tool.poetry.group.docs.dependencies] mkdocs-material = "^9.5.30" mkdocs-include-markdown-plugin = "^3.9.1" -mkdocstrings = "^0.22.0" +mkdocstrings = {extras = ["python"], version = "^0.25.2"} pymdown-extensions = "^10.1" mkdocs-swagger-ui-tag = "^0.6.10" diff --git a/src/pydase/client/client.py b/src/pydase/client/client.py index f8db2dd..7371ab0 100644 --- a/src/pydase/client/client.py +++ b/src/pydase/client/client.py @@ -43,10 +43,10 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection): via a socket.io client in an asyncio environment. Args: - sio_client (socketio.AsyncClient): + sio_client: The socket.io client instance used for asynchronous communication with the pydase service server. - loop (asyncio.AbstractEventLoop): + loop: The event loop in which the client operations are managed and executed. This class is used to create a proxy object that behaves like a local representation @@ -54,20 +54,20 @@ class ProxyClass(ProxyClassMixin, pydase.components.DeviceConnection): while actually communicating over network protocols. It can also be used as an attribute of a pydase service itself, e.g. - ```python - import pydase + ```python + import pydase - class MyService(pydase.DataService): - proxy = pydase.Client( - hostname="...", port=8001, block_until_connected=False - ).proxy + class MyService(pydase.DataService): + proxy = pydase.Client( + hostname="...", port=8001, block_until_connected=False + ).proxy - if __name__ == "__main__": - service = MyService() - server = pydase.Server(service, web_port=8002).run() - ``` + if __name__ == "__main__": + service = MyService() + server = pydase.Server(service, web_port=8002).run() + ``` """ def __init__( @@ -84,19 +84,16 @@ class Client: connection, disconnection, and updates, and ensures that the proxy object is up-to-date with the server state. - Attributes: - proxy (ProxyClass): - A proxy object representing the remote service, facilitating interaction as - if it were local. - Args: - url (str): + url: The URL of the pydase Socket.IO server. This should always contain the protocol and the hostname. + Examples: - - wss://my-service.example.com # for secure connections, use wss - - ws://localhost:8001 - block_until_connected (bool): + + - `wss://my-service.example.com` # for secure connections, use wss + - `ws://localhost:8001` + block_until_connected: If set to True, the constructor will block until the connection to the service has been established. This is useful for ensuring the client is ready to use immediately after instantiation. Default is True. @@ -112,6 +109,8 @@ class Client: self._sio = socketio.AsyncClient() self._loop = asyncio.new_event_loop() self.proxy = ProxyClass(sio_client=self._sio, loop=self._loop) + """A proxy object representing the remote service, facilitating interaction as + if it were local.""" self._thread = threading.Thread( target=asyncio_loop_thread, args=(self._loop,), daemon=True ) diff --git a/src/pydase/components/coloured_enum.py b/src/pydase/components/coloured_enum.py index 9c33539..0829c2b 100644 --- a/src/pydase/components/coloured_enum.py +++ b/src/pydase/components/coloured_enum.py @@ -7,58 +7,59 @@ class ColouredEnum(Enum): This class extends the standard Enum but requires its values to be valid CSS colour codes. Supported colour formats include: - - Hexadecimal colours - - Hexadecimal colours with transparency - - RGB colours - - RGBA colours - - HSL colours - - HSLA colours - - Predefined/Cross-browser colour names + + - Hexadecimal colours + - Hexadecimal colours with transparency + - RGB colours + - RGBA colours + - HSL colours + - HSLA colours + - Predefined/Cross-browser colour names + Refer to the this website for more details on colour formats: (https://www.w3schools.com/cssref/css_colours_legal.php) The behavior of this component in the UI depends on how it's defined in the data service: - - As property with a setter or as attribute: Renders as a dropdown menu, - allowing users to select and change its value from the frontend. - - As property without a setter: Displays as a coloured box with the key of the - `ColouredEnum` as text inside, serving as a visual indicator without user - interaction. + + - As property with a setter or as attribute: Renders as a dropdown menu, allowing + users to select and change its value from the frontend. + - As property without a setter: Displays as a coloured box with the key of the + `ColouredEnum` as text inside, serving as a visual indicator without user + interaction. Example: - -------- - ```python - import pydase.components as pyc - import pydase + ```python + import pydase.components as pyc + import pydase - class MyStatus(pyc.ColouredEnum): - PENDING = "#FFA500" # Orange - RUNNING = "#0000FF80" # Transparent Blue - PAUSED = "rgb(169, 169, 169)" # Dark Gray - RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow - COMPLETED = "hsl(120, 100%, 50%)" # Green - FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red - CANCELLED = "SlateGray" # Slate Gray + class MyStatus(pyc.ColouredEnum): + PENDING = "#FFA500" # Orange + RUNNING = "#0000FF80" # Transparent Blue + PAUSED = "rgb(169, 169, 169)" # Dark Gray + RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow + COMPLETED = "hsl(120, 100%, 50%)" # Green + FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red + CANCELLED = "SlateGray" # Slate Gray - class StatusExample(pydase.DataService): - _status = MyStatus.RUNNING + class StatusExample(pydase.DataService): + _status = MyStatus.RUNNING - @property - def status(self) -> MyStatus: - return self._status + @property + def status(self) -> MyStatus: + return self._status - @status.setter - def status(self, value: MyStatus) -> None: - # Custom logic here... - self._status = value + @status.setter + def status(self, value: MyStatus) -> None: + # Custom logic here... + self._status = value - # Example usage: - my_service = StatusExample() - my_service.status = MyStatus.FAILED - ``` + # Example usage: + my_service = StatusExample() + my_service.status = MyStatus.FAILED + ``` - Note - ---- - Each enumeration name and value must be unique. This means that you should use - different colour formats when you want to use a colour multiple times. + Note: + Each enumeration name and value must be unique. This means that you should use + different colour formats when you want to use a colour multiple times. """ diff --git a/src/pydase/components/device_connection.py b/src/pydase/components/device_connection.py index 2a941fe..cf98a62 100644 --- a/src/pydase/components/device_connection.py +++ b/src/pydase/components/device_connection.py @@ -19,22 +19,26 @@ class DeviceConnection(pydase.data_service.DataService): to the device. This method should update the `self._connected` attribute to reflect the connection status: - >>> class MyDeviceConnection(DeviceConnection): - ... def connect(self) -> None: - ... # Implementation to connect to the device - ... # Update self._connected to `True` if connection is successful, - ... # `False` otherwise - ... ... + ```python + class MyDeviceConnection(DeviceConnection): + def connect(self) -> None: + # Implementation to connect to the device + # Update self._connected to `True` if connection is successful, + # `False` otherwise + ... + ``` Optionally, if additional logic is needed to determine the connection status, the `connected` property can also be overridden: - >>> class MyDeviceConnection(DeviceConnection): - ... @property - ... def connected(self) -> bool: - ... # Custom logic to determine connection status - ... return some_custom_condition - ... + ```python + class MyDeviceConnection(DeviceConnection): + @property + def connected(self) -> bool: + # Custom logic to determine connection status + return some_custom_condition + + ``` Frontend Representation ----------------------- diff --git a/src/pydase/components/number_slider.py b/src/pydase/components/number_slider.py index 4ffbe96..e804f1c 100644 --- a/src/pydase/components/number_slider.py +++ b/src/pydase/components/number_slider.py @@ -11,76 +11,74 @@ class NumberSlider(DataService): This class models a UI slider for a data service, allowing for adjustments of a parameter within a specified range and increments. - Parameters: - ----------- - value (float, optional): - The initial value of the slider. Defaults to 0. - min (float, optional): - The minimum value of the slider. Defaults to 0. - max (float, optional): - The maximum value of the slider. Defaults to 100. - step_size (float, optional): - The increment/decrement step size of the slider. Defaults to 1.0. + Args: + value: + The initial value of the slider. Defaults to 0. + min_: + The minimum value of the slider. Defaults to 0. + max_: + The maximum value of the slider. Defaults to 100. + step_size: + The increment/decrement step size of the slider. Defaults to 1.0. Example: - -------- - ```python - class MySlider(pydase.components.NumberSlider): - def __init__( - self, - value: float = 0.0, - min_: float = 0.0, - max_: float = 100.0, - step_size: float = 1.0, - ) -> None: - super().__init__(value, min_, max_, step_size) + ```python + class MySlider(pydase.components.NumberSlider): + def __init__( + self, + value: float = 0.0, + min_: float = 0.0, + max_: float = 100.0, + step_size: float = 1.0, + ) -> None: + super().__init__(value, min_, max_, step_size) - @property - def min(self) -> float: - return self._min + @property + def min(self) -> float: + return self._min - @min.setter - def min(self, value: float) -> None: - self._min = value + @min.setter + def min(self, value: float) -> None: + self._min = value - @property - def max(self) -> float: - return self._max + @property + def max(self) -> float: + return self._max - @max.setter - def max(self, value: float) -> None: - self._max = value + @max.setter + def max(self, value: float) -> None: + self._max = value - @property - def step_size(self) -> float: - return self._step_size + @property + def step_size(self) -> float: + return self._step_size - @step_size.setter - def step_size(self, value: float) -> None: - self._step_size = value + @step_size.setter + def step_size(self, value: float) -> None: + self._step_size = value - @property - def value(self) -> float: - return self._value + @property + def value(self) -> float: + return self._value - @value.setter - def value(self, value: float) -> None: - if value < self._min or value > self._max: - raise ValueError( - "Value is either below allowed min or above max value." - ) + @value.setter + def value(self, value: float) -> None: + if value < self._min or value > self._max: + raise ValueError( + "Value is either below allowed min or above max value." + ) - self._value = value + self._value = value - class MyService(pydase.DataService): - def __init__(self) -> None: - self.voltage = MyService() + class MyService(pydase.DataService): + def __init__(self) -> None: + self.voltage = MyService() - # Modifying or accessing the voltage value: - my_service = MyService() - my_service.voltage.value = 5 - print(my_service.voltage.value) # Output: 5 - ``` + # Modifying or accessing the voltage value: + my_service = MyService() + my_service.voltage.value = 5 + print(my_service.voltage.value) # Output: 5 + ``` """ def __init__( diff --git a/src/pydase/config.py b/src/pydase/config.py index f3dd066..dabdd45 100644 --- a/src/pydase/config.py +++ b/src/pydase/config.py @@ -6,18 +6,30 @@ from confz import BaseConfig, EnvSource class OperationMode(BaseConfig): # type: ignore[misc] environment: Literal["testing", "development", "production"] = "development" + """The service's operation mode.""" CONFIG_SOURCES = EnvSource(allow=["ENVIRONMENT"]) class ServiceConfig(BaseConfig): # type: ignore[misc] + """Service configuration. + + Variables can be set through environment variables prefixed with `SERVICE_` or an + `.env` file containing those variables. + """ + config_dir: Path = Path("config") + """Configuration directory""" web_port: int = 8001 + """Web server port""" CONFIG_SOURCES = EnvSource(allow_all=True, prefix="SERVICE_", file=".env") class WebServerConfig(BaseConfig): # type: ignore[misc] + """The service's web server configuration.""" + generate_web_settings: bool = False + """Should generate web_settings.json file""" CONFIG_SOURCES = EnvSource(allow=["GENERATE_WEB_SETTINGS"]) diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index 958c930..479d601 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -124,8 +124,10 @@ class DataServiceObserver(PropertyObserver): object. Args: - callback (Callable[[str, Any, dict[str, Any]]): The callback function to be - registered. The function should have the following signature: + callback: + The callback function to be registered. The function should have the + following signature: + - full_access_path (str): The full dot-notation access path of the changed attribute. This path indicates the location of the changed attribute within the observable object's structure. diff --git a/src/pydase/data_service/state_manager.py b/src/pydase/data_service/state_manager.py index b5f7a89..13f93a7 100644 --- a/src/pydase/data_service/state_manager.py +++ b/src/pydase/data_service/state_manager.py @@ -33,17 +33,19 @@ def load_state(func: Callable[..., Any]) -> Callable[..., Any]: the value should be loaded from the JSON file. Example: - >>> class Service(pydase.DataService): - ... _name = "Service" - ... - ... @property - ... def name(self) -> str: - ... return self._name - ... - ... @name.setter - ... @load_state - ... def name(self, value: str) -> None: - ... self._name = value + ```python + class Service(pydase.DataService): + _name = "Service" + + @property + def name(self) -> str: + return self._name + + @name.setter + @load_state + def name(self, value: str) -> None: + self._name = value + ``` """ func._load_state = True # type: ignore[attr-defined] @@ -85,13 +87,11 @@ class StateManager: StateManager provides a snapshot of the DataService's state that is sufficiently accurate for initial rendering and interaction. - Attributes: - cache (dict[str, Any]): - A dictionary cache of the DataService's state. - filename (str): - The file name used for storing the DataService's state. - service (DataService): + Args: + service: The DataService instance whose state is being managed. + filename: + The file name used for storing the DataService's state. Note: The StateManager's cache updates are triggered by notifications and do not @@ -200,9 +200,11 @@ class StateManager: It also handles type-specific conversions for the new value before setting it. Args: - path: A dot-separated string indicating the hierarchical path to the + path: + A dot-separated string indicating the hierarchical path to the attribute. - value: The new value to set for the attribute. + serialized_value: + The serialized representation of the new value to set for the attribute. """ try: diff --git a/src/pydase/observer_pattern/observable/decorators.py b/src/pydase/observer_pattern/observable/decorators.py index fe21f54..1aa65be 100644 --- a/src/pydase/observer_pattern/observable/decorators.py +++ b/src/pydase/observer_pattern/observable/decorators.py @@ -17,10 +17,10 @@ def validate_set( getter and check against the desired value. Args: - timeout (float): + timeout: The maximum time (in seconds) to wait for the value to be within the precision boundary. - precision (float | None): + precision: The acceptable deviation from the desired value. If None, the value must be exact. """ @@ -44,13 +44,11 @@ def has_validate_set_decorator(prop: property) -> bool: Checks if a property setter has been decorated with the `validate_set` decorator. Args: - prop (property): + prop: The property to check. Returns: - bool: - True if the property setter has the `validate_set` decorator, False - otherwise. + True if the property setter has the `validate_set` decorator, False otherwise. """ property_setter = prop.fset @@ -68,11 +66,11 @@ def _validate_value_was_correctly_set( specified `precision` and time `timeout`. Args: - obj (Observable): + obj: The instance of the class containing the property. - name (str): + name: The name of the property to validate. - value (Any): + value: The desired value to check against. Raises: diff --git a/src/pydase/server/server.py b/src/pydase/server/server.py index 5908e25..9c701d7 100644 --- a/src/pydase/server/server.py +++ b/src/pydase/server/server.py @@ -35,18 +35,18 @@ class AdditionalServerProtocol(Protocol): Args: data_service_observer: - Observer for the DataService, handling state updates and communication to - connected clients through injected callbacks. Can be utilized to access the - service and state manager, and to add custom state-update callbacks. + Observer for the DataService, handling state updates and communication to + connected clients through injected callbacks. Can be utilized to access the + service and state manager, and to add custom state-update callbacks. host: - Hostname or IP address where the server is accessible. Commonly '0.0.0.0' to - bind to all network interfaces. + Hostname or IP address where the server is accessible. Commonly '0.0.0.0' to + bind to all network interfaces. port: - Port number on which the server listens. Typically in the range 1024-65535 - (non-standard ports). + Port number on which the server listens. Typically in the range 1024-65535 + (non-standard ports). **kwargs: - Any additional parameters required for initializing the server. These - parameters are specific to the server's implementation. + Any additional parameters required for initializing the server. These + parameters are specific to the server's implementation. """ def __init__( @@ -64,18 +64,17 @@ class AdditionalServerProtocol(Protocol): class AdditionalServer(TypedDict): - """ - A TypedDict that represents the configuration for an additional server to be run + """A TypedDict that represents the configuration for an additional server to be run alongside the main server. - - This class is used to specify the server type, the port on which the server should - run, and any additional keyword arguments that should be passed to the server when - it's instantiated. """ server: type[AdditionalServerProtocol] + """Server adhering to the + [`AdditionalServerProtocol`][pydase.server.server.AdditionalServerProtocol].""" port: int + """Port on which the server should run.""" kwargs: dict[str, Any] + """Additional keyword arguments that will be passed to the server's constructor """ class Server: @@ -83,29 +82,20 @@ class Server: The `Server` class provides a flexible server implementation for the `DataService`. Args: - service: DataService + service: The DataService instance that this server will manage. - host: str - The host address for the server. Default is '0.0.0.0', which means all + host: + The host address for the server. Defaults to `'0.0.0.0'`, which means all available network interfaces. - web_port: int - The port number for the web server. Default is - `pydase.config.ServiceConfig().web_port`. - enable_web: bool - Whether to enable the web server. Default is True. - filename: str | Path | None + web_port: + The port number for the web server. Defaults to + [`ServiceConfig().web_port`][pydase.config.ServiceConfig.web_port]. + enable_web: + Whether to enable the web server. + filename: Filename of the file managing the service state persistence. - Defaults to None. - additional_servers : list[AdditionalServer] - A list of additional servers to run alongside the main server. Each entry in - the list should be a dictionary with the following structure: - - server: A class that adheres to the AdditionalServerProtocol. This - class should have an `__init__` method that accepts the DataService - instance, port, host, and optional keyword arguments, and a `serve` - method that is a coroutine responsible for starting the server. - - port: The port on which the additional server will be running. - - kwargs: A dictionary containing additional keyword arguments that will - be passed to the server's `__init__` method. + additional_servers: + A list of additional servers to run alongside the main server. Here's an example of how you might define an additional server: @@ -145,8 +135,8 @@ class Server: ) server.run() ``` - **kwargs: Any - Additional keyword arguments. + **kwargs: + Additional keyword arguments. """ def __init__( # noqa: PLR0913 @@ -214,7 +204,7 @@ class Server: ) server_task = self._loop.create_task(addin_server.serve()) - server_task.add_done_callback(self.handle_server_shutdown) + server_task.add_done_callback(self._handle_server_shutdown) self.servers[server_name] = server_task if self._enable_web: self._web_server = WebServer( @@ -225,10 +215,10 @@ class Server: ) server_task = self._loop.create_task(self._web_server.serve()) - server_task.add_done_callback(self.handle_server_shutdown) + server_task.add_done_callback(self._handle_server_shutdown) self.servers["web"] = server_task - def handle_server_shutdown(self, task: asyncio.Task[Any]) -> None: + def _handle_server_shutdown(self, task: asyncio.Task[Any]) -> None: """Handle server shutdown. If the service should exit, do nothing. Else, make the service exit.""" diff --git a/src/pydase/server/web_server/sio_setup.py b/src/pydase/server/web_server/sio_setup.py index 097834d..20b1f07 100644 --- a/src/pydase/server/web_server/sio_setup.py +++ b/src/pydase/server/web_server/sio_setup.py @@ -54,12 +54,15 @@ class RunMethodDict(TypedDict): exposed DataService. Attributes: - name (str): The name of the method to be run. - parent_path (str): The access path for the parent object of the method to be - run. This is used to construct the full access path for the method. For - example, for an method with access path 'attr1.list_attr[0].method_name', - 'attr1.list_attr[0]' would be the parent_path. - kwargs (dict[str, Any]): The arguments passed to the method. + name: + The name of the method to be run. + parent_path: + The access path for the parent object of the method to be run. This is used + to construct the full access path for the method. For example, for an method + with access path 'attr1.list_attr[0].method_name', 'attr1.list_attr[0]' + would be the parent_path. + kwargs: + The arguments passed to the method. """ name: str @@ -76,15 +79,15 @@ def setup_sio_server( Sets up and configures a Socket.IO asynchronous server. Args: - observer (DataServiceObserver): - The observer managing state updates and communication. - enable_cors (bool): - Flag indicating whether CORS should be enabled for the server. - loop (asyncio.AbstractEventLoop): - The event loop in which the server will run. + observer: + The observer managing state updates and communication. + enable_cors: + Flag indicating whether CORS should be enabled for the server. + loop: + The event loop in which the server will run. Returns: - socketio.AsyncServer: The configured Socket.IO asynchronous server. + The configured Socket.IO asynchronous server. """ state_manager = observer.state_manager diff --git a/src/pydase/server/web_server/web_server.py b/src/pydase/server/web_server/web_server.py index d6d994c..370faec 100644 --- a/src/pydase/server/web_server/web_server.py +++ b/src/pydase/server/web_server/web_server.py @@ -25,41 +25,50 @@ API_VERSION = "v1" class WebServer: """ - Represents a web server that adheres to the AdditionalServerProtocol, designed to - work with a DataService instance. This server facilitates client-server - communication and state management through web protocols and socket connections. + Represents a web server that adheres to the + [`AdditionalServerProtocol`][pydase.server.server.AdditionalServerProtocol], + designed to work with a [`DataService`][pydase.DataService] instance. This server + facilitates client-server communication and state management through web protocols + and socket connections. - The WebServer class initializes and manages a web server environment using FastAPI - and Socket.IO, allowing for HTTP and WebSocket communications. It incorporates CORS - (Cross-Origin Resource Sharing) support, custom CSS, and serves a frontend static - files directory. It also initializes web server settings based on configuration - files or generates default settings if necessary. + The WebServer class initializes and manages a web server environment aiohttp and + Socket.IO, allowing for HTTP and Socket.IO communications. It incorporates CORS + (Cross-Origin Resource Sharing) support, custom CSS, and serves a static files + directory. It also initializes web server settings based on configuration files or + generates default settings if necessary. Configuration for the web server (like service configuration directory and whether to generate new web settings) is determined in the following order of precedence: + 1. Values provided directly to the constructor. 2. Environment variable settings (via configuration classes like - `pydase.config.ServiceConfig` and `pydase.config.WebServerConfig`). + [`ServiceConfig`][pydase.config.ServiceConfig] and + [`WebServerConfig`][pydase.config.WebServerConfig]). 3. Default values defined in the configuration classes. Args: - data_service_observer (DataServiceObserver): Observer for the DataService, - handling state updates and communication to connected clients. - host (str): Hostname or IP address where the server is accessible. Commonly - '0.0.0.0' to bind to all network interfaces. - port (int): Port number on which the server listens. Typically in the range - 1024-65535 (non-standard ports). - css (str | Path | None, optional): Path to a custom CSS file for styling the - frontend. If None, no custom styles are applied. Defaults to None. - enable_cors (bool, optional): Flag to enable or disable CORS policy. When True, - CORS is enabled, allowing cross-origin requests. Defaults to True. - config_dir (Path | None, optional): Path to the configuration - directory where the web settings will be stored. Defaults to - `pydase.config.ServiceConfig().config_dir`. - generate_new_web_settings (bool | None, optional): Flag to enable or disable - generation of new web settings if the configuration file is missing. Defaults - to `pydase.config.WebServerConfig().generate_new_web_settings`. - **kwargs (Any): Additional unused keyword arguments. + data_service_observer: + Observer for the [`DataService`][pydase.DataService], handling state updates and communication to + connected clients. + host: + Hostname or IP address where the server is accessible. Commonly '0.0.0.0' + to bind to all network interfaces. + port: + Port number on which the server listens. Typically in the range 1024-65535 + (non-standard ports). + css: + Path to a custom CSS file for styling the frontend. If None, no custom + styles are applied. Defaults to None. + enable_cors: + Flag to enable or disable CORS policy. When True, CORS is enabled, allowing + cross-origin requests. Defaults to True. + config_dir: + Path to the configuration directory where the web settings will be stored. + Defaults to [`ServiceConfig().config_dir`][pydase.config.ServiceConfig.config_dir]. + generate_web_settings: + Flag to enable or disable generation of new web settings if the + configuration file is missing. Defaults to + [`WebServerConfig().generate_web_settings`][pydase.config.WebServerConfig.generate_web_settings]. """ def __init__( # noqa: PLR0913 diff --git a/src/pydase/units.py b/src/pydase/units.py index 9938b66..0f3c245 100644 --- a/src/pydase/units.py +++ b/src/pydase/units.py @@ -21,18 +21,20 @@ def convert_to_quantity( Convert a given value into a pint.Quantity object with the specified unit. Args: - value (QuantityDict | float | int | Quantity): + value: 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. + unit: + 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. + The converted value as a pint.Quantity object with the specified unit. Examples: >>> convert_to_quantity(5, 'm') @@ -42,9 +44,9 @@ def convert_to_quantity( >>> convert_to_quantity(10.0 * u.units.V) - Notes: - - If unit is not provided and value is a float or int, the resulting Quantity - will be unitless. + Note: + If unit is not provided and value is a float or int, the resulting Quantity will + be unitless. """ if isinstance(value, int | float): diff --git a/src/pydase/utils/decorators.py b/src/pydase/utils/decorators.py index 95b9fa3..7bbf45c 100644 --- a/src/pydase/utils/decorators.py +++ b/src/pydase/utils/decorators.py @@ -10,9 +10,9 @@ class FunctionDefinitionError(Exception): def frontend(func: Callable[..., Any]) -> Callable[..., Any]: - """ - Decorator to mark a DataService method for frontend rendering. Ensures that the - method does not contain arguments, as they are not supported for frontend rendering. + """Decorator to mark a [`DataService`][pydase.DataService] method for frontend + rendering. Ensures that the method does not contain arguments, as they are not + supported for frontend rendering. """ if function_has_arguments(func): diff --git a/src/pydase/utils/serialization/deserializer.py b/src/pydase/utils/serialization/deserializer.py index 17a9807..0515002 100644 --- a/src/pydase/utils/serialization/deserializer.py +++ b/src/pydase/utils/serialization/deserializer.py @@ -19,10 +19,15 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +import json + +json.loads + class Deserializer: @classmethod def deserialize(cls, serialized_object: SerializedObject) -> Any: + """Deserialize `serialized_object` (a `dict`) to a Python object.""" type_handler: dict[str | None, None | Callable[..., Any]] = { None: None, "int": cls.deserialize_primitive, @@ -159,4 +164,5 @@ class Deserializer: def loads(serialized_object: SerializedObject) -> Any: + """Deserialize `serialized_object` (a `dict`) to a Python object.""" return Deserializer.deserialize(serialized_object) diff --git a/src/pydase/utils/serialization/serializer.py b/src/pydase/utils/serialization/serializer.py index ab3e8f7..0633842 100644 --- a/src/pydase/utils/serialization/serializer.py +++ b/src/pydase/utils/serialization/serializer.py @@ -52,8 +52,27 @@ class SerializationPathError(Exception): class Serializer: + """Serializes objects into + [`SerializedObject`][pydase.utils.serialization.types.SerializedObject] + representations. + """ + @classmethod def serialize_object(cls, obj: Any, access_path: str = "") -> SerializedObject: # noqa: C901 + """Serialize `obj` to a + [`SerializedObject`][pydase.utils.serialization.types.SerializedObject]. + + Args: + obj: + Object to be serialized. + access_path: + String corresponding to the full access path of the object. This will be + prepended to the full_access_path in the SerializedObject entries. + + Returns: + Dictionary representation of `obj`. + """ + result: SerializedObject if isinstance(obj, Exception): @@ -313,6 +332,19 @@ class Serializer: def dump(obj: Any) -> SerializedObject: + """Serialize `obj` to a + [`SerializedObject`][pydase.utils.serialization.types.SerializedObject]. + + The [`Serializer`][pydase.utils.serialization.serializer.Serializer] is used for + encoding. + + Args: + obj: + Object to be serialized. + + Returns: + Dictionary representation of `obj`. + """ return Serializer.serialize_object(obj) @@ -321,12 +353,13 @@ def set_nested_value_by_path( ) -> None: """ Set a value in a nested dictionary structure, which conforms to the serialization - format used by `pydase.utils.serializer.Serializer`, using a dot-notation path. + format used by [`Serializer`][pydase.utils.serialization.serializer.Serializer], + using a dot-notation path. Args: serialization_dict: The base dictionary representing data serialized with - `pydase.utils.serializer.Serializer`. + [`Serializer`][pydase.utils.serialization.serializer.Serializer]. path: The dot-notation path (e.g., 'attr1.attr2[0].attr3') indicating where to set the value. @@ -334,8 +367,8 @@ def set_nested_value_by_path( The new value to set at the specified path. Note: - - If the index equals the length of the list, the function will append the - serialized representation of the 'value' to the list. + If the index equals the length of the list, the function will append the + serialized representation of the 'value' to the list. """ path_parts = parse_full_access_path(path) @@ -438,26 +471,24 @@ def get_container_item_by_key( ) -> SerializedObject: """ Retrieve an item from a container specified by the passed key. Add an item to the - container if allow_append is set to True. + container if `allow_append` is set to `True`. If specified keys or indexes do not exist, the function can append new elements to dictionaries and to lists if `allow_append` is True and the missing element is exactly the next sequential index (for lists). Args: - container: dict[str, SerializedObject] | list[SerializedObject] + container: The container representing serialized data. - key: str + key: The key name representing the attribute in the dictionary, which may include direct keys or indexes (e.g., 'attr_name', '["key"]' or '[0]'). - allow_append: bool + allow_append: Flag to allow appending a new entry if the specified index is out of range by exactly one position. Returns: - SerializedObject - The dictionary or list item corresponding to the specified attribute and - index. + The dictionary or list item corresponding to the specified attribute and index. Raises: SerializationPathError: @@ -485,13 +516,12 @@ def get_data_paths_from_serialized_object( # noqa: C901 Recursively extracts full access paths from a serialized object. Args: - serialized_obj (SerializedObject): + serialized_obj: The dictionary representing the serialization of an object. Produced by `pydase.utils.serializer.Serializer`. Returns: - list[str]: - A list of strings, each representing a full access path in the serialized + A list of strings, each representing a full access path in the serialized object. """ @@ -532,12 +562,11 @@ def generate_serialized_data_paths( Recursively extracts full access paths from a serialized DataService class instance. Args: - data (dict[str, SerializedObject]): + data: The value of the "value" key of a serialized DataService class instance. Returns: - list[str]: - A list of strings, each representing a full access path in the serialized + A list of strings, each representing a full access path in the serialized object. """ @@ -556,3 +585,6 @@ def serialized_dict_is_nested_object(serialized_dict: SerializedObject) -> bool: # We are excluding Quantity here as the value corresponding to the "value" key is # a dictionary of the form {"magnitude": ..., "unit": ...} return serialized_dict["type"] != "Quantity" and (isinstance(value, dict | list)) + + +__all__ = ["Serializer", "dump"] diff --git a/src/pydase/utils/serialization/types.py b/src/pydase/utils/serialization/types.py index a02a988..ab25580 100644 --- a/src/pydase/utils/serialization/types.py +++ b/src/pydase/utils/serialization/types.py @@ -123,3 +123,21 @@ SerializedObject = ( | SerializedQuantity | SerializedNoValue ) +""" +This type can be any of the following: + +- SerializedBool +- SerializedFloat +- SerializedInteger +- SerializedString +- SerializedDatetime +- SerializedList +- SerializedDict +- SerializedNoneType +- SerializedMethod +- SerializedException +- SerializedDataService +- SerializedEnum +- SerializedQuantity +- SerializedNoValue +"""