timer.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import asyncio
  2. import time
  3. from contextlib import nullcontext
  4. from typing import Any, Callable, Optional
  5. from .. import background_tasks, core, helpers
  6. from ..binding import BindableProperty
  7. from ..client import Client
  8. from ..element import Element
  9. from ..logging import log
  10. class Timer(Element, component='timer.js'):
  11. active = BindableProperty()
  12. interval = BindableProperty()
  13. def __init__(self,
  14. interval: float,
  15. callback: Callable[..., Any], *,
  16. active: bool = True,
  17. once: bool = False,
  18. ) -> None:
  19. """Timer
  20. One major drive behind the creation of NiceGUI was the necessity to have a simple approach to update the interface in regular intervals,
  21. for example to show a graph with incoming measurements.
  22. A timer will execute a callback repeatedly with a given interval.
  23. :param interval: the interval in which the timer is called (can be changed during runtime)
  24. :param callback: function or coroutine to execute when interval elapses
  25. :param active: whether the callback should be executed or not (can be changed during runtime)
  26. :param once: whether the callback is only executed once after a delay specified by `interval` (default: `False`)
  27. """
  28. super().__init__()
  29. self.interval = interval
  30. self.callback: Optional[Callable[..., Any]] = callback
  31. self.active = active
  32. self._is_canceled: bool = False
  33. coroutine = self._run_once if once else self._run_in_loop
  34. if core.app.is_started:
  35. background_tasks.create(coroutine(), name=str(callback))
  36. else:
  37. core.app.on_startup(coroutine)
  38. def activate(self) -> None:
  39. """Activate the timer."""
  40. assert not self._is_canceled, 'Cannot activate a canceled timer'
  41. self.active = True
  42. def deactivate(self) -> None:
  43. """Deactivate the timer."""
  44. self.active = False
  45. def cancel(self) -> None:
  46. """Cancel the timer."""
  47. self._is_canceled = True
  48. async def _run_once(self) -> None:
  49. try:
  50. if not await self._connected():
  51. return
  52. with self.parent_slot or nullcontext():
  53. await asyncio.sleep(self.interval)
  54. if self.active and not self._should_stop():
  55. await self._invoke_callback()
  56. finally:
  57. self._cleanup()
  58. async def _run_in_loop(self) -> None:
  59. try:
  60. if not await self._connected():
  61. return
  62. with self.parent_slot or nullcontext():
  63. while not self._should_stop():
  64. try:
  65. start = time.time()
  66. if self.active:
  67. await self._invoke_callback()
  68. dt = time.time() - start
  69. await asyncio.sleep(self.interval - dt)
  70. except asyncio.CancelledError:
  71. break
  72. except Exception as e:
  73. core.app.handle_exception(e)
  74. await asyncio.sleep(self.interval)
  75. finally:
  76. self._cleanup()
  77. async def _invoke_callback(self) -> None:
  78. try:
  79. assert self.callback is not None
  80. result = self.callback()
  81. if helpers.is_coroutine_function(self.callback):
  82. await result
  83. except Exception as e:
  84. core.app.handle_exception(e)
  85. async def _connected(self, timeout: float = 60.0) -> bool:
  86. """Wait for the client connection before the timer callback can be allowed to manipulate the state.
  87. See https://github.com/zauberzeug/nicegui/issues/206 for details.
  88. Returns True if the client is connected, False if the client is not connected and the timer should be cancelled.
  89. """
  90. if self.client.shared:
  91. return True
  92. # ignore served pages which do not reconnect to backend (e.g. monitoring requests, scrapers etc.)
  93. try:
  94. await self.client.connected(timeout=timeout)
  95. return True
  96. except TimeoutError:
  97. log.error(f'Timer cancelled because client is not connected after {timeout} seconds')
  98. return False
  99. def _should_stop(self) -> bool:
  100. return (
  101. self.is_deleted or
  102. self.client.id not in Client.instances or
  103. self._is_canceled or
  104. core.app.is_stopping or
  105. core.app.is_stopped
  106. )
  107. def _cleanup(self) -> None:
  108. self.callback = None