"""nave_scheduler.py — il pezzo lato Nave: decide QUANDO lanciare cosa. In produzione, questo non gira come loop proprio — i tempi vengono da CronCreate (strumento di scheduling nativo di Adrian, sveglia periodica), che ai momenti giusti chiama check_and_trigger() con l'ora corrente. Qui in sandbox si simula passando un "now" arbitrario, per verificare la logica di matching cron senza aspettare orari veri. Gli orari vivono in schedule.json, in questa stessa cartella — non più in `schedules` (tabella frank.db), deciso con Mauro il 07/08/2026: come jobs/ è l'allowlist di cosa si può eseguire, schedule.json è la fonte di verità di quando. Un solo file, modificabile da Nave, sotto git. """ import json import uuid from datetime import datetime from pathlib import Path from croniter import croniter DEFAULT_SCHEDULE_PATH = Path(__file__).resolve().parent / "schedule.json" def load_schedule_config(schedule_path: Path = DEFAULT_SCHEDULE_PATH) -> dict: """Ritorna {job_name: cron_string} da schedule.json. Dizionario vuoto se il file non esiste ancora (nessuna schedulazione definita).""" if not schedule_path.exists(): return {} return json.loads(schedule_path.read_text(encoding="utf-8")) class NaveScheduler: def __init__(self, to_dir: Path, schedule_config: dict = None): """schedule_config: {job_name: cron_string}. Se omesso, carica da schedule.json (comportamento di default in produzione).""" self.to_dir = Path(to_dir) self.schedule_config = schedule_config if schedule_config is not None else load_schedule_config() def is_due(self, job_name: str, now: datetime) -> bool: cron = self.schedule_config[job_name] # croniter.match verifica se "now" (troncato al minuto) coincide # con l'espressione cron — non "e' passato da quando", proprio # "e' questo il minuto giusto". return croniter.match(cron, now.replace(second=0, microsecond=0)) def check_and_trigger(self, now: datetime) -> list[str]: """Per ogni job dovuto a 'now', scrive una richiesta run_job in to_dir. Ritorna i nomi dei job innescati in questo giro.""" self.to_dir.mkdir(parents=True, exist_ok=True) triggered = [] for job_name in self.schedule_config: if self.is_due(job_name, now): req_id = uuid.uuid4().hex[:8] req = { "id": req_id, "action": "run_job", "params": {"job_name": job_name}, } (self.to_dir / f"richiesta-{req_id}.json").write_text( json.dumps(req, ensure_ascii=False), encoding="utf-8" ) triggered.append(job_name) return triggered