diff --git a/docs/dev-guide/api.md b/docs/dev-guide/api.md index 83ed19f..d6b4ef3 100644 --- a/docs/dev-guide/api.md +++ b/docs/dev-guide/api.md @@ -13,6 +13,12 @@ ::: pydase.components handler: python +::: pydase.task + handler: python + options: + inherited_members: false + show_submodules: true + ::: pydase.utils.serialization.serializer handler: python diff --git a/docs/user-guide/Tasks.md b/docs/user-guide/Tasks.md index 3f62ed9..dbacdd1 100644 --- a/docs/user-guide/Tasks.md +++ b/docs/user-guide/Tasks.md @@ -1,20 +1,18 @@ # 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. - -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: +`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: ```python import pydase +from pydase.task.decorator import task class SensorService(pydase.DataService): def __init__(self): super().__init__() self.readout_frequency = 1.0 - self._autostart_tasks["read_sensor_data"] = () def _process_data(self, data: ...) -> None: ... @@ -22,6 +20,7 @@ class SensorService(pydase.DataService): def _read_from_sensor(self) -> Any: ... + @task(autostart=True) async def read_sensor_data(self): while True: data = self._read_from_sensor() @@ -34,6 +33,6 @@ if __name__ == "__main__": 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. -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. +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. +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. diff --git a/frontend/src/components/GenericComponent.tsx b/frontend/src/components/GenericComponent.tsx index 67e442e..31d3cba 100644 --- a/frontend/src/components/GenericComponent.tsx +++ b/frontend/src/components/GenericComponent.tsx @@ -4,7 +4,6 @@ import { NumberComponent, NumberObject } from "./NumberComponent"; import { SliderComponent } from "./SliderComponent"; import { EnumComponent } from "./EnumComponent"; import { MethodComponent } from "./MethodComponent"; -import { AsyncMethodComponent } from "./AsyncMethodComponent"; import { StringComponent } from "./StringComponent"; import { ListComponent } from "./ListComponent"; import { DataServiceComponent, DataServiceJSON } from "./DataServiceComponent"; @@ -17,6 +16,7 @@ import { updateValue } from "../socket"; import { DictComponent } from "./DictComponent"; import { parseFullAccessPath } from "../utils/stateUtils"; import { SerializedEnum, SerializedObject } from "../types/SerializedObject"; +import { TaskComponent, TaskStatus } from "./TaskComponent"; interface GenericComponentProps { attribute: SerializedObject; @@ -144,30 +144,16 @@ export const GenericComponent = React.memo( /> ); } else if (attribute.type === "method") { - if (!attribute.async) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); } else if (attribute.type === "str") { return ( ); + } else if (attribute.type == "Task") { + return ( + + ); } else if (attribute.type === "DataService") { return ( void; displayName: string; id: string; - render: boolean; } -export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => { - const { - fullAccessPath, - docString, - value: runningTask, - addNotification, - displayName, - id, - } = props; - - // Conditional rendering based on the 'render' prop. - if (!props.render) { - return null; - } +export const TaskComponent = React.memo((props: TaskProps) => { + const { fullAccessPath, docString, status, addNotification, displayName, id } = props; const renderCount = useRenderCount(); const formRef = useRef(null); const [spinning, setSpinning] = useState(false); - const name = fullAccessPath.split(".").at(-1)!; - const parentPath = fullAccessPath.slice(0, -(name.length + 1)); useEffect(() => { let message: string; - if (runningTask === null) { - message = `${fullAccessPath} task was stopped.`; - } else { + if (status === "RUNNING") { message = `${fullAccessPath} was started.`; + } else { + message = `${fullAccessPath} was stopped.`; } + addNotification(message); setSpinning(false); - }, [props.value]); + }, [status]); const execute = async (event: React.FormEvent) => { event.preventDefault(); - let method_name: string; - if (runningTask !== undefined && runningTask !== null) { - method_name = `stop_${name}`; - } else { - method_name = `start_${name}`; - } + const method_name = status == "RUNNING" ? "stop" : "start"; - const accessPath = [parentPath, method_name].filter((element) => element).join("."); + const accessPath = [fullAccessPath, method_name] + .filter((element) => element) + .join("."); setSpinning(true); runMethod(accessPath); }; return ( -
+
{process.env.NODE_ENV === "development" &&
Render count: {renderCount}
}
@@ -76,7 +60,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {