Files
upbeatBytes/goodnews/daily.py
T
thejayman77 a7da8362ab Small joys backend: shared daily framework + On This Day engine
- goodnews/daily.py: shared helpers for the daily "small joys" (http_json, date-seeded
  deterministic pick, dedup key) so each joy is a small self-contained module.
- goodnews/onthisday.py: harvest today's MM-DD from Wikimedia's On-this-day feed →
  tone-filter to good/neutral (keyword floor + optional LLM refine) → pool → deterministic
  daily pick (idempotent, respects blocked/featured) → cached row. Network/LLM before any
  DB write. Multi-source ready (source column).
- db.py: onthisday_pool + daily_onthisday tables.
- api.py: GET /api/onthisday/today (edge-cacheable).
- cli.py: cycle step (run after Daily Art; --no-joys to skip), LLM client for tone refine.
- tests/test_onthisday.py: 7 tests (filter+dedup, pick idempotent, blocked/featured,
  never-empty, empty-pool, LLM-narrow). 382 backend tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 16:51:29 -04:00

40 lines
1.5 KiB
Python

"""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 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]