← refacto-feature-scout
"""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")