import asyncio import time from collections.abc import Awaitable from contextlib import AbstractContextManager, nullcontext from typing import Any, Callable, Optional from . import background_tasks, core from .awaitable_response import AwaitableResponse from .binding import BindableProperty class Timer: active = BindableProperty() interval = BindableProperty() def __init__(self, interval: float, callback: Callable[..., Any], *, active: bool = True, once: bool = False, immediate: bool = True, ) -> None: """Timer One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals, for example to show a graph with incoming measurements. A timer will execute a callback repeatedly with a given interval. :param interval: the interval in which the timer is called (can be changed during runtime) :param callback: function or coroutine to execute when interval elapses :param active: whether the callback should be executed or not (can be changed during runtime) :param once: whether the callback is only executed once after a delay specified by `interval` (default: `False`) :param immediate: whether the callback should be executed immediately (default: `True`, ignored if `once` is `True`, *added in version 2.9.0*) """ super().__init__() self.interval = interval self.callback: Optional[Callable[..., Any]] = callback self.active = active self._is_canceled = False self._immediate = immediate self._current_invocation: Optional[asyncio.Task] = None coroutine = self._run_once if once else self._run_in_loop if core.is_script_mode_preflight(): return if core.app.is_started: background_tasks.create(coroutine(), name=str(callback)) else: core.app.on_startup(coroutine) def _get_context(self) -> AbstractContextManager: return nullcontext() def activate(self) -> None: """Activate the timer.""" assert not self._is_canceled, 'Cannot activate a canceled timer' self.active = True def deactivate(self) -> None: """Deactivate the timer.""" self.active = False def cancel(self, *, with_current_invocation: bool = False) -> None: """Cancel the timer. :param with_current_invocation: whether to cancel the currently invoked task of the callback (*added in version 2.23.0*) """ self._is_canceled = True if with_current_invocation and self._current_invocation is not None: self._current_invocation.cancel() async def _run_once(self) -> None: try: if not await self._can_start(): return with self._get_context(): await asyncio.sleep(self.interval) if self.active and not self._should_stop(): self._current_invocation = asyncio.create_task(self._invoke_callback()) await self._current_invocation finally: self._cleanup() async def _run_in_loop(self) -> None: try: if not self._immediate: await asyncio.sleep(self.interval) if not await self._can_start(): return with self._get_context(): while not self._should_stop(): try: start = time.time() if self.active: self._current_invocation = asyncio.create_task(self._invoke_callback()) await self._current_invocation dt = time.time() - start await asyncio.sleep(self.interval - dt) except asyncio.CancelledError: break except Exception as e: core.app.handle_exception(e) await asyncio.sleep(self.interval) finally: self._cleanup() async def _invoke_callback(self) -> None: with self._get_context(): try: assert self.callback is not None result = self.callback() if isinstance(result, Awaitable) and not isinstance(result, AwaitableResponse): await result except Exception as e: core.app.handle_exception(e) async def _can_start(self) -> bool: return True def _should_stop(self) -> bool: return ( self._is_canceled or core.app.is_stopping or core.app.is_stopped ) def _cleanup(self) -> None: self.callback = None