#!/usr/bin/env python3 # ============================================================================= # QWHPI — Quebec Weekly Housing Price Index # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : ops/scheduler/refresh.py # Purpose : Weekly refresh job — re-run the incremental pipeline every Monday # 06:00, write a run manifest, load the DB, log structured events. # ============================================================================= """Weekly refresh scheduler. Every Monday 06:00 (container time): ingest new rows → re-clean (append-only) → re-estimate monthly indexes → new data_vintage → canonical rebuild → DB load. A JSON run manifest (input hash, row counts, timings) is written to ``ops/scheduler/runs/``. The API's parquet cache invalidates automatically on file mtime; ETag values include the data vintage, so HTTP caches roll over. """ from __future__ import annotations import json import logging import subprocess import sys import time from datetime import datetime, timezone from pathlib import Path import schedule WORKSPACE = Path("/workspace") RUNS_DIR = WORKSPACE / "ops" / "scheduler" / "runs" logging.basicConfig( level=logging.INFO, format='{"ts":"%(asctime)s","level":"%(levelname)s","msg":"%(message)s"}', ) log = logging.getLogger("qwhpi.scheduler") STEPS = [ "engine/scripts/03_clean.py", "engine/scripts/09_monthly.py", "engine/scripts/11_monthly_canonical.py", ] def run_refresh() -> None: started = time.time() manifest: dict = {"started_at": datetime.now(timezone.utc).isoformat(), "steps": []} ok = True for step in STEPS: t0 = time.time() proc = subprocess.run([sys.executable, str(WORKSPACE / step)], cwd=WORKSPACE, capture_output=True, text=True) manifest["steps"].append({ "step": step, "seconds": round(time.time() - t0, 1), "returncode": proc.returncode, }) if proc.returncode != 0: log.error("step %s failed: %s", step, proc.stderr[-2000:]) ok = False break log.info("step %s ok (%.0fs)", step, time.time() - t0) if ok: sys.path.insert(0, str(WORKSPACE / "engine" / "src")) try: from qwhpi.export import input_hash, load_database manifest["input_hash"] = input_hash() manifest["db_loaded"] = load_database() except Exception as exc: # noqa: BLE001 log.error("db load failed: %s", exc) manifest["db_loaded"] = False manifest["ok"] = ok manifest["total_seconds"] = round(time.time() - started, 1) RUNS_DIR.mkdir(parents=True, exist_ok=True) stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ") (RUNS_DIR / f"run_{stamp}.json").write_text(json.dumps(manifest, indent=2)) log.info("refresh finished ok=%s in %.0fs", ok, manifest["total_seconds"]) if __name__ == "__main__": log.info("scheduler up — weekly refresh Mondays 06:00") schedule.every().monday.at("06:00").do(run_refresh) while True: schedule.run_pending() time.sleep(60)