Skip to content

Commit

Permalink
start to implement app.timer()
Browse files Browse the repository at this point in the history
  • Loading branch information
falkoschindler committed Dec 11, 2024
1 parent 3cfb14a commit ff93d39
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 0 deletions.
7 changes: 7 additions & 0 deletions nicegui/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from ..server import Server
from ..staticfiles import CacheControlledStaticFiles
from ..storage import Storage
from ..timer import Timer
from .app_config import AppConfig
from .range_response import get_range_response

Expand Down Expand Up @@ -79,6 +80,12 @@ def stop(self) -> None:
Client.auto_index_client.safe_invoke(t)
self._state = State.STOPPED

def timer(self, interval: float, handler: Callable) -> Timer:
"""Create a timer that repeats the given handler at the given interval."""
timer = Timer(interval, handler)
timer.start()
return timer

def on_connect(self, handler: Union[Callable, Awaitable]) -> None:
"""Called every time a new client connects to NiceGUI.
Expand Down
91 changes: 91 additions & 0 deletions nicegui/timer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import asyncio
import time
from typing import Awaitable, Callable, ClassVar

from . import background_tasks, core, logging
from .awaitable_response import AwaitableResponse


class Timer:
tasks: ClassVar[set[asyncio.Task]] = set()

def __init__(self, interval: float, handler: Callable) -> None:
self.handler = handler
self.interval = interval
self._task: asyncio.Task | None = None

def start(self) -> None:
"""Start the timer."""
if self.running:
return
if core.app.is_started or core.app.is_starting:
self._task = background_tasks.create(self._repeat())
self.tasks.add(self._task)
elif self.start not in core.app._startup_handlers: # pylint: disable=protected-access
print('add to startup handlers')
core.app.on_startup(self.start)

async def _repeat(self) -> None:
await asyncio.sleep(self.interval) # TODO: sleep immediately?
while True:
start = time.time()
try:
if core.app.is_stopping:
logging.log.info('%s must be stopped', self.handler)
break
await self._invoke_callback()
dt = time.time() - start
except (asyncio.CancelledError, GeneratorExit):
return
except Exception:
dt = time.time() - start
logging.log.exception('error in "%s"', self.handler.__qualname__)
if self.interval == 0 and dt < 0.1:
delay = 0.1 - dt
logging.log.warning(
f'"{self.handler.__qualname__}" would be called to frequently ' +
f'because it only took {dt*1000:.0f} ms; ' +
f'delaying this step for {delay*1000:.0f} ms')
await asyncio.sleep(delay)
try:
await asyncio.sleep(self.interval - dt)
except (asyncio.CancelledError, GeneratorExit):
return

async def _invoke_callback(self) -> None:
try:
assert self.handler is not None
result = self.handler()
if isinstance(result, Awaitable) and not isinstance(result, AwaitableResponse):
await result
except Exception as e:
core.app.handle_exception(e)

def stop(self) -> None:
"""Stop the timer."""
if not self._task:
return

if not self._task.done():
self._task.cancel()

self.tasks.remove(self._task)
self._task = None

@property
def running(self) -> bool:
"""Whether the timer is running."""
return self._task is not None and not self._task.done()

@running.setter
def running(self, value: bool) -> None:
if value:
self.start()
else:
self.stop()

@staticmethod
def stop_all() -> None:
"""Stop all timers."""
for timer in Timer.tasks:
timer.cancel()

0 comments on commit ff93d39

Please sign in to comment.