Download app/watcher.py from drowzeys/keys-Auto-Receipts-Studio: direct link, hf CLI and curl.
- Browser
- Download file 4.06 kB
-
https://huggingface.co/drowzeys/keys-Auto-Receipts-Studio/resolve/main/app/watcher.py
- Command line
-
hf download hf://drowzeys/keys-Auto-Receipts-Studio/app/watcher.py
-
curl -L -o watcher.py https://huggingface.co/drowzeys/keys-Auto-Receipts-Studio/resolve/main/app/watcher.py
4.06 kB
| from __future__ import annotations | |
| import threading | |
| import time | |
| from collections.abc import Callable | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from watchdog.events import FileSystemEvent, FileSystemEventHandler | |
| from watchdog.observers import Observer | |
| from app.config import ACCEPTED_SUFFIXES, Settings | |
| from app.pipeline import process_file | |
| OnBatch = Callable[[list[Path]], None] | |
| def is_ignored(path: Path) -> bool: | |
| name = path.name | |
| if name.startswith("."): | |
| return True | |
| if name.endswith(".tmp") or name.endswith(".part"): | |
| return True | |
| if ".syncthing." in name or name.startswith(".syncthing"): | |
| return True | |
| return path.suffix.lower() not in ACCEPTED_SUFFIXES | |
| class _State: | |
| sig: tuple[int, float] | |
| last_change: float | |
| class IdleBatchWatcher: | |
| """Settle files until size+mtime are unchanged for idle_seconds, then batch.""" | |
| def __init__( | |
| self, | |
| inbox: Path, | |
| *, | |
| idle_seconds: float = 30.0, | |
| on_batch: OnBatch | None = None, | |
| ) -> None: | |
| self.inbox = Path(inbox) | |
| self.idle_seconds = idle_seconds | |
| self.on_batch = on_batch | |
| self._state: dict[Path, _State] = {} | |
| self._lock = threading.Lock() | |
| self._running = False | |
| self._observer: Observer | None = None | |
| self._thread: threading.Thread | None = None | |
| def note(self, path: Path, now: float) -> None: | |
| if not path.is_file() or is_ignored(path): | |
| return | |
| stat = path.stat() | |
| sig = (stat.st_size, stat.st_mtime) | |
| with self._lock: | |
| prev = self._state.get(path) | |
| if prev is None or prev.sig != sig: | |
| self._state[path] = _State(sig=sig, last_change=now) | |
| def tick(self, now: float | None = None) -> list[Path]: | |
| clock = time.monotonic() if now is None else now | |
| self.inbox.mkdir(parents=True, exist_ok=True) | |
| for path in self.inbox.iterdir(): | |
| self.note(path, clock) | |
| ready: list[Path] = [] | |
| with self._lock: | |
| for path, state in list(self._state.items()): | |
| if not path.is_file(): | |
| self._state.pop(path, None) | |
| continue | |
| if clock - state.last_change >= self.idle_seconds: | |
| ready.append(path) | |
| self._state.pop(path, None) | |
| if ready and self.on_batch is not None: | |
| self.on_batch(ready) | |
| return ready | |
| def start(self) -> None: | |
| if self._running: | |
| return | |
| self._running = True | |
| handler = _Handler(self) | |
| observer = Observer() | |
| observer.schedule(handler, str(self.inbox), recursive=False) | |
| observer.start() | |
| self._observer = observer | |
| self._thread = threading.Thread(target=self._loop, daemon=True) | |
| self._thread.start() | |
| def stop(self) -> None: | |
| self._running = False | |
| if self._observer is not None: | |
| self._observer.stop() | |
| self._observer.join(timeout=2) | |
| self._observer = None | |
| def _loop(self) -> None: | |
| while self._running: | |
| self.tick() | |
| time.sleep(min(0.25, max(0.05, self.idle_seconds / 4))) | |
| class _Handler(FileSystemEventHandler): | |
| def __init__(self, watcher: IdleBatchWatcher) -> None: | |
| self.watcher = watcher | |
| def on_any_event(self, event: FileSystemEvent) -> None: | |
| if event.is_directory: | |
| return | |
| path = Path(str(event.src_path)) | |
| self.watcher.note(path, time.monotonic()) | |
| def process_batch(paths: list[Path], settings: Settings) -> None: | |
| for path in paths: | |
| try: | |
| process_file(path, settings) | |
| except Exception: | |
| continue | |
| def start_inbox_watcher(settings: Settings) -> IdleBatchWatcher: | |
| watcher = IdleBatchWatcher( | |
| settings.inbox_dir, | |
| idle_seconds=settings.idle_seconds, | |
| on_batch=lambda paths: process_batch(paths, settings), | |
| ) | |
| watcher.start() | |
| return watcher | |