timer.py 3.8 KB

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