Advertisement

← refacto-feature-scout

scout/watch.py

2.4 KB Download file
"""Leave it running and it looks after itself.

One command, in the foreground, that does what the operator's cron does on a server: look for
ideas once a day, and every few minutes get on with whatever has been approved. It stops when
the window is closed, which is the right way round for something that spends a person's money
and changes their code.
"""
from __future__ import annotations

import time
from datetime import datetime
from typing import Any, Callable

from .locks import Busy, Lock
from .projects import Project
from .store import Store

SCAN_HOUR = 8
SCAN_MINUTE = 15
POLL_MINUTES = 5


def scanned_today(store: Store, project: Project, now: float) -> bool:
    run = store.last_run(f"scan:{project.key}")
    if not run or not run.get("started_at"):
        return False
    started = datetime.fromtimestamp(float(run["started_at"])).date()
    return started == datetime.fromtimestamp(now).date()


def due_for_a_scan(store: Store, project: Project, now: float, *, hour: int = SCAN_HOUR,
                   minute: int = SCAN_MINUTE) -> bool:
    """Time of day, not a timer: a laptop that was asleep at the hour still catches up."""
    when = datetime.fromtimestamp(now)
    if (when.hour, when.minute) < (hour, minute):
        return False
    return not scanned_today(store, project, now)


def watch(projects: list[Project], store: Store, *, scanner: Callable[..., Any],
          poller: Callable[..., Any], say: Callable[[str], None] = print,
          now: Callable[[], float] = time.time, sleep: Callable[[float], None] = time.sleep,
          hour: int = SCAN_HOUR, minute: int = SCAN_MINUTE,
          poll_minutes: int = POLL_MINUTES, once: bool = False) -> None:
    while True:
        moment = now()
        due = [p for p in projects if due_for_a_scan(store, p, moment, hour=hour, minute=minute)]
        try:
            with Lock(wait_s=0):
                if due:
                    say(f"{_clock(moment)} looking for ideas for "
                        f"{', '.join(p.name for p in due)}...")
                    scanner(store, due)
                poller(store)
        except Busy:
            pass                      # something else is mid-job; try again next time round
        if once:
            return
        sleep(poll_minutes * 60)


def _clock(moment: float) -> str:
    return datetime.fromtimestamp(moment).strftime("%H:%M")