a7da8362ab
- 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>
40 lines
1.5 KiB
Python
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]
|