from __future__ import annotations import asyncio import logging from dataclasses import dataclass from typing import Callable, Coroutine logger = logging.getLogger(__name__) @dataclass class ScheduledTask: name: str coro_factory: Callable[[], Coroutine] interval_seconds: int run_on_startup: bool = False async def start_scheduler(tasks: list[ScheduledTask]) -> None: """Run scheduled tasks in a loop. Call with asyncio.create_task().""" last_run: dict[str, float] = {} for task in tasks: if task.run_on_startup: logger.info(f"Startup task: {task.name}") asyncio.create_task(_run_task(task)) last_run[task.name] = asyncio.get_event_loop().time() while True: now = asyncio.get_event_loop().time() for task in tasks: last = last_run.get(task.name, 0) if now - last >= task.interval_seconds: asyncio.create_task(_run_task(task)) last_run[task.name] = now await asyncio.sleep(1) async def _run_task(task: ScheduledTask) -> None: try: await task.coro_factory() except Exception: logger.exception(f"Scheduled task {task.name!r} raised an exception")