timer.py 4.0 KB

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