docs: updates Task documentation

- updates Tasks.md
- updates docstrings
- adds api section
This commit is contained in:
Mose Müller 2024-09-16 14:36:01 +02:00
parent 0c95b5e3cb
commit ece68b4b99
6 changed files with 125 additions and 17 deletions

View File

@ -13,6 +13,12 @@
::: pydase.components ::: pydase.components
handler: python handler: python
::: pydase.task
handler: python
options:
inherited_members: false
show_submodules: true
::: pydase.utils.serialization.serializer ::: pydase.utils.serialization.serializer
handler: python handler: python

View File

@ -1,20 +1,18 @@
# Understanding Tasks # Understanding Tasks
In `pydase`, a task is defined as an asynchronous function without arguments contained in a class that inherits from `pydase.DataService`. These tasks usually contain a while loop and are designed to carry out periodic functions. In `pydase`, a task is defined as an asynchronous function without arguments that is decorated with the `@task` decorator and contained in a class that inherits from `pydase.DataService`. These tasks usually contain a while loop and are designed to carry out periodic functions. For example, a task might be used to periodically read sensor data, update a database, or perform any other recurring job.
For example, a task might be used to periodically read sensor data, update a database, or perform any other recurring job. One core feature of `pydase` is its ability to automatically generate start and stop functions for these tasks. This allows you to control task execution via both the frontend and python clients, giving you flexible and powerful control over your service's operation. `pydase` allows you to control task execution via both the frontend and Python clients and can automatically start tasks upon initialization of the service. By using the `@task` decorator with the `autostart=True` argument in your service class, `pydase` will automatically start these tasks when the server is started. Here's an example:
Another powerful feature of `pydase` is its ability to automatically start tasks upon initialization of the service. By specifying the tasks and their arguments in the `_autostart_tasks` dictionary in your service class's `__init__` method, `pydase` will automatically start these tasks when the server is started. Here's an example:
```python ```python
import pydase import pydase
from pydase.task.decorator import task
class SensorService(pydase.DataService): class SensorService(pydase.DataService):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.readout_frequency = 1.0 self.readout_frequency = 1.0
self._autostart_tasks["read_sensor_data"] = ()
def _process_data(self, data: ...) -> None: def _process_data(self, data: ...) -> None:
... ...
@ -22,6 +20,7 @@ class SensorService(pydase.DataService):
def _read_from_sensor(self) -> Any: def _read_from_sensor(self) -> Any:
... ...
@task(autostart=True)
async def read_sensor_data(self): async def read_sensor_data(self):
while True: while True:
data = self._read_from_sensor() data = self._read_from_sensor()
@ -34,6 +33,6 @@ if __name__ == "__main__":
pydase.Server(service=service).run() pydase.Server(service=service).run()
``` ```
In this example, `read_sensor_data` is a task that continuously reads data from a sensor. By adding it to the `_autostart_tasks` dictionary, it will automatically start running when `pydase.Server(service).run()` is executed. In this example, `read_sensor_data` is a task that continuously reads data from a sensor. By decorating it with `@task(autostart=True)`, it will automatically start running when `pydase.Server(service).run()` is executed.
As with all tasks, `pydase` will generate `start_read_sensor_data` and `stop_read_sensor_data` methods, which can be called to manually start and stop the data reading task. The readout frequency can be updated using the `readout_frequency` attribute.
The `@task` decorator replaces the function with a task object that has `start()` and `stop()` methods. This means you can control the task execution directly using these methods. For instance, you can manually start or stop the task by calling `service.read_sensor_data.start()` and `service.read_sensor_data.stop()`, respectively.

View File

@ -12,7 +12,8 @@ def autostart_service_tasks(
"""Starts the service tasks defined with the `autostart` keyword argument. """Starts the service tasks defined with the `autostart` keyword argument.
This method goes through the attributes of the passed service and its nested This method goes through the attributes of the passed service and its nested
`pydase.DataService` instances and calls the start method on autostart-tasks. [`DataService`][pydase.DataService] instances and calls the start method on
autostart-tasks.
""" """
for attr in dir(service): for attr in dir(service):

View File

@ -18,6 +18,54 @@ def task(
], ],
Task[R], Task[R],
]: ]:
"""
A decorator to define a function as a task within a
[`DataService`][pydase.DataService] class.
This decorator transforms an asynchronous function into a
[`Task`][pydase.task.task.Task] object. The `Task` object provides methods like
`start()` and `stop()` to control the execution of the task.
Tasks are typically used to perform periodic or recurring jobs, such as reading
sensor data, updating databases, or other operations that need to be repeated over
time.
Args:
autostart:
If set to True, the task will automatically start when the service is
initialized. Defaults to False.
Returns:
A decorator that converts an asynchronous function into a
[`Task`][pydase.task.task.Task] object.
Example:
```python
import asyncio
import pydase
from pydase.task.decorator import task
class MyService(pydase.DataService):
@task(autostart=True)
async def my_task(self) -> None:
while True:
# Perform some periodic work
await asyncio.sleep(1)
if __name__ == "__main__":
service = MyService()
pydase.Server(service=service).run()
```
In this example, `my_task` is defined as a task using the `@task` decorator, and
it will start automatically when the service is initialized because
`autostart=True` is set. You can manually start or stop the task using
`service.my_task.start()` and `service.my_task.stop()`, respectively.
"""
def decorator( def decorator(
func: Callable[[Any], Coroutine[None, None, R]] func: Callable[[Any], Coroutine[None, None, R]]
| Callable[[], Coroutine[None, None, R]], | Callable[[], Coroutine[None, None, R]],

View File

@ -36,6 +36,53 @@ def is_bound_method(
class Task(pydase.data_service.data_service.DataService, Generic[R]): class Task(pydase.data_service.data_service.DataService, Generic[R]):
"""
A class representing a task within the `pydase` framework.
The `Task` class wraps an asynchronous function and provides methods to manage its
lifecycle, such as `start()` and `stop()`. It is typically used to perform periodic
or recurring jobs in a [`DataService`][pydase.DataService], like reading
sensor data, updating databases, or executing other background tasks.
When a function is decorated with the [`@task`][pydase.task.decorator.task]
decorator, it is replaced by a `Task` instance that controls the execution of the
original function.
Args:
func:
The asynchronous function that this task wraps. It must be a coroutine
without arguments.
autostart:
If set to True, the task will automatically start when the service is
initialized. Defaults to False.
Example:
```python
import asyncio
import pydase
from pydase.task.decorator import task
class MyService(pydase.DataService):
@task(autostart=True)
async def my_task(self) -> None:
while True:
# Perform some periodic work
await asyncio.sleep(1)
if __name__ == "__main__":
service = MyService()
pydase.Server(service=service).run()
```
In this example, `my_task` is defined as a task using the `@task` decorator, and
it will start automatically when the service is initialized because
`autostart=True` is set. You can manually start or stop the task using
`service.my_task.start()` and `service.my_task.stop()`, respectively.
"""
def __init__( def __init__(
self, self,
func: Callable[[Any], Coroutine[None, None, R | None]] func: Callable[[Any], Coroutine[None, None, R | None]]
@ -59,23 +106,26 @@ class Task(pydase.data_service.data_service.DataService, Generic[R]):
@property @property
def autostart(self) -> bool: def autostart(self) -> bool:
"""Defines if the task should be started automatically when the `pydase.Server` """Defines if the task should be started automatically when the
starts.""" [`Server`][pydase.Server] starts."""
return self._autostart return self._autostart
@property @property
def status(self) -> TaskStatus: def status(self) -> TaskStatus:
"""Returns the current status of the task."""
return self._status return self._status
def start(self) -> None: def start(self) -> None:
"""Starts the asynchronous task if it is not already running."""
if self._task: if self._task:
return return
def task_done_callback(task: asyncio.Task[R | None]) -> None: def task_done_callback(task: asyncio.Task[R | None]) -> None:
"""Handles tasks that have finished. """Handles tasks that have finished.
Update task status, calls the defined callbacks, and logs and re-raises Updates the task status, calls the defined callbacks, and logs and re-raises
exceptions.""" exceptions.
"""
self._task = None self._task = None
self._status = TaskStatus.NOT_RUNNING self._status = TaskStatus.NOT_RUNNING
@ -113,6 +163,8 @@ class Task(pydase.data_service.data_service.DataService, Generic[R]):
self._task.add_done_callback(task_done_callback) self._task.add_done_callback(task_done_callback)
def stop(self) -> None: def stop(self) -> None:
"""Stops the running asynchronous task by cancelling it."""
if self._task: if self._task:
self._task.cancel() self._task.cancel()
@ -120,11 +172,11 @@ class Task(pydase.data_service.data_service.DataService, Generic[R]):
"""Descriptor method used to correctly set up the task. """Descriptor method used to correctly set up the task.
This descriptor method is called by the class instance containing the task. This descriptor method is called by the class instance containing the task.
We need to use this descriptor to bind the task function to that class instance. It binds the task function to that class instance.
As the __init__ function is called when a function is decorated with Since the `__init__` function is called when a function is decorated with
@pydase.task.task, we should delay some of the setup until this descriptor [`@task`][pydase.task.decorator.task], some setup is delayed until this
function is called. descriptor function is called.
""" """
if instance and not self._set_up: if instance and not self._set_up:

View File

@ -2,5 +2,7 @@ import enum
class TaskStatus(enum.Enum): class TaskStatus(enum.Enum):
"""Possible statuses of a [`Task`][pydase.task.task.Task]."""
RUNNING = "running" RUNNING = "running"
NOT_RUNNING = "not_running" NOT_RUNNING = "not_running"