lib.watcher

Observador reactivo de ficheros YAML.

Usa watchdog (inotify en Linux, FSEvents en macOS, ReadDirectoryChanges en Windows) para detectar cambios en tiempo real sin polling.

Uso:

from lib.watcher import start_watch, WatchdogNotAvailable

def on_change(yaml_path): rebuild(yaml_path)

try: start_watch([Path('courses/java/content')], on_change) except WatchdogNotAvailable: print("instala watchdog: uv sync --group watch")

  1"""
  2Observador reactivo de ficheros YAML.
  3
  4Usa watchdog (inotify en Linux, FSEvents en macOS, ReadDirectoryChanges en Windows)
  5para detectar cambios en tiempo real sin polling.
  6
  7Uso:
  8    from lib.watcher import start_watch, WatchdogNotAvailable
  9
 10    def on_change(yaml_path):
 11        rebuild(yaml_path)
 12
 13    try:
 14        start_watch([Path('courses/java/content')], on_change)
 15    except WatchdogNotAvailable:
 16        print("instala watchdog: uv sync --group watch")
 17"""
 18
 19from __future__ import annotations
 20
 21import threading
 22from pathlib import Path
 23from typing import Callable
 24
 25try:
 26    from watchdog.observers import Observer
 27    from watchdog.events import FileSystemEventHandler, FileModifiedEvent
 28    WATCHDOG_AVAILABLE = True
 29except ImportError:
 30    WATCHDOG_AVAILABLE = False
 31    Observer = None                    # type: ignore[assignment,misc]
 32    FileSystemEventHandler = object    # type: ignore[assignment,misc]
 33
 34
 35class WatchdogNotAvailable(RuntimeError):
 36    """Se lanza si watchdog no está instalado."""
 37
 38
 39class _YamlChangeHandler(FileSystemEventHandler):
 40    """Dispara un callback con debounce cuando cambia un YAML vigilado."""
 41
 42    def __init__(
 43        self,
 44        yaml_set: set[Path],
 45        on_change: Callable[[Path], None],
 46        debounce_s: float = 0.3,
 47    ) -> None:
 48        super().__init__()
 49        self._yaml_set   = yaml_set
 50        self._on_change  = on_change
 51        self._debounce   = debounce_s
 52        self._pending: set[Path] = set()
 53        self._lock  = threading.Lock()
 54        self._timer: threading.Timer | None = None
 55
 56    def on_modified(self, event) -> None:
 57        if getattr(event, 'is_directory', False):
 58            return
 59        p = Path(event.src_path).resolve()
 60        if p in self._yaml_set:
 61            self._schedule(p)
 62
 63    def _schedule(self, path: Path) -> None:
 64        with self._lock:
 65            self._pending.add(path)
 66            if self._timer is not None:
 67                self._timer.cancel()
 68            self._timer = threading.Timer(self._debounce, self._flush)
 69            self._timer.start()
 70
 71    def _flush(self) -> None:
 72        with self._lock:
 73            to_build = list(self._pending)
 74            self._pending.clear()
 75        for p in to_build:
 76            try:
 77                self._on_change(p)
 78            except Exception as exc:
 79                import sys
 80                print(f'[watch] Error en {p.name}: {exc}', file=sys.stderr)
 81
 82    def cancel_pending(self) -> None:
 83        with self._lock:
 84            if self._timer is not None:
 85                self._timer.cancel()
 86
 87
 88def start_watch(
 89    yaml_paths: list[Path],
 90    on_change: Callable[[Path], None],
 91    debounce_s: float = 0.3,
 92) -> None:
 93    """Inicia el observer y bloquea hasta Ctrl+C.
 94
 95    Args:
 96        yaml_paths: Lista de ficheros YAML a vigilar.
 97        on_change:  Callback llamado con el Path del YAML modificado.
 98        debounce_s: Segundos de espera para agrupar cambios rápidos.
 99
100    Raises:
101        WatchdogNotAvailable: si watchdog no está instalado.
102    """
103    if not WATCHDOG_AVAILABLE:
104        raise WatchdogNotAvailable(
105            "watchdog no está instalado.\n"
106            "Instalar con: uv sync --group watch"
107        )
108
109    resolved = [p.resolve() for p in yaml_paths]
110    yaml_set = set(resolved)
111    dirs     = {p.parent for p in resolved}
112
113    handler  = _YamlChangeHandler(yaml_set, on_change, debounce_s)
114    observer = Observer()
115    for d in dirs:
116        observer.schedule(handler, str(d), recursive=False)
117
118    observer.start()
119    try:
120        observer.join()
121    except KeyboardInterrupt:
122        observer.stop()
123        handler.cancel_pending()
class WatchdogNotAvailable(builtins.RuntimeError):
36class WatchdogNotAvailable(RuntimeError):
37    """Se lanza si watchdog no está instalado."""

Se lanza si watchdog no está instalado.

def start_watch( yaml_paths: list[pathlib.Path], on_change: Callable[[pathlib.Path], NoneType], debounce_s: float = 0.3) -> None:
 89def start_watch(
 90    yaml_paths: list[Path],
 91    on_change: Callable[[Path], None],
 92    debounce_s: float = 0.3,
 93) -> None:
 94    """Inicia el observer y bloquea hasta Ctrl+C.
 95
 96    Args:
 97        yaml_paths: Lista de ficheros YAML a vigilar.
 98        on_change:  Callback llamado con el Path del YAML modificado.
 99        debounce_s: Segundos de espera para agrupar cambios rápidos.
100
101    Raises:
102        WatchdogNotAvailable: si watchdog no está instalado.
103    """
104    if not WATCHDOG_AVAILABLE:
105        raise WatchdogNotAvailable(
106            "watchdog no está instalado.\n"
107            "Instalar con: uv sync --group watch"
108        )
109
110    resolved = [p.resolve() for p in yaml_paths]
111    yaml_set = set(resolved)
112    dirs     = {p.parent for p in resolved}
113
114    handler  = _YamlChangeHandler(yaml_set, on_change, debounce_s)
115    observer = Observer()
116    for d in dirs:
117        observer.schedule(handler, str(d), recursive=False)
118
119    observer.start()
120    try:
121        observer.join()
122    except KeyboardInterrupt:
123        observer.stop()
124        handler.cancel_pending()

Inicia el observer y bloquea hasta Ctrl+C.

Arguments:
  • yaml_paths: Lista de ficheros YAML a vigilar.
  • on_change: Callback llamado con el Path del YAML modificado.
  • debounce_s: Segundos de espera para agrupar cambios rápidos.
Raises:
  • WatchdogNotAvailable: si watchdog no está instalado.