timer.py 4.4 KB

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