"""Shared helpers for the daily "small joys" features — On This Day, Word of the Day, Quote of the Day (and whatever calm little delights come next). Each joy keeps its own pool + daily table, but they all share this skeleton: harvest -> pool → deterministic daily pick (date-seeded, least-recently-shown) → cache row in a daily_* table → API → page. This module holds only the genuinely shared bits (network + the deterministic pick), so a new joy is a small self-contained module, not a copy-paste of plumbing. Network calls go through http_json so tests can monkeypatch them. """ from __future__ import annotations import hashlib import json import urllib.request _UA = {"User-Agent": "upbeatBytes/1.0 (+https://upbeatbytes.com)"} def http_json(url: str, timeout: int = 20) -> dict: req = urllib.request.Request(url, headers=_UA) with urllib.request.urlopen(req, timeout=timeout) as r: return json.loads(r.read().decode("utf-8")) def seeded_order(ids: list, date_str: str) -> list: """Rotate a list deterministically by the date, so the day's pick is the same for everyone and varies day to day (the same trick Daily Art uses).""" if not ids: return ids seed = int(hashlib.sha256(date_str.encode()).hexdigest(), 16) % len(ids) return ids[seed:] + ids[:seed] def freshest(rows: list) -> list: """The cohort to feature today, from pool rows carrying `id` + `shown_at`: every NEVER-shown item (shown_at NULL) while any remain, else every item tied for the OLDEST shown_at. Guarantees each pool item is featured ONCE before any repeat, then cycles oldest-first. Pick deterministically *within* this cohort (seeded_order) — NEVER across the whole pool, which re-feeds recent items (the QOTD/WOTD repeat bug).""" never = [r["id"] for r in rows if r["shown_at"] is None] if never: return sorted(never) if not rows: return [] oldest = min(r["shown_at"] for r in rows) return sorted(r["id"] for r in rows if r["shown_at"] == oldest) def content_key(*parts) -> str: """A stable dedup key for a pool item (so re-harvesting never duplicates a row).""" raw = "|".join("" if p is None else str(p) for p in parts) return hashlib.sha256(raw.encode()).hexdigest()[:24]