Wiki Jokes: daily LLM jokes from Wikipedia featured article

- FastAPI + APScheduler + SQLite, single container
- Daily generation at 06:00 Europe/Copenhagen + cold-start generation
- Retry with backoff (3 attempts/15 min), stale fallback with banner
- OpenAI-compatible endpoint via env (OPENAI_BASE_URL/MODEL/API_KEY)
- docker-compose with named volume for joke persistence
This commit is contained in:
2026-09-29 19:38:01 +00:00
commit 075b72ec6c
14 changed files with 601 additions and 0 deletions
+111
View File
@@ -0,0 +1,111 @@
import logging
from contextlib import asynccontextmanager
from datetime import date
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.interval import IntervalTrigger
from fastapi import FastAPI
from fastapi.responses import HTMLResponse, JSONResponse
from zoneinfo import ZoneInfo
from . import db
from .config import settings
from .generator import generation_exhausted, run_generation, today_local
from .templates import PAGE_TEMPLATE
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
log = logging.getLogger("jokes.app")
scheduler = AsyncIOScheduler(timezone=ZoneInfo(settings.timezone))
async def tick() -> None:
"""Runs every RETRY_MINUTES: generate today's jokes if missing,
unless we've exhausted retries for today."""
day = today_local()
if db.get_day(day) is not None:
return
if generation_exhausted(day):
return
run_generation()
@asynccontextmanager
async def lifespan(app: FastAPI):
db.init_db()
# Cold start: generate immediately if today's jokes are missing.
if db.get_day(today_local()) is None:
log.info("Cold start: generating today's jokes now")
run_generation()
# Daily schedule.
scheduler.add_job(
tick,
CronTrigger(
hour=settings.generate_hour,
minute=settings.generate_minute,
timezone=ZoneInfo(settings.timezone),
),
id="daily-generation",
)
# Retry loop (no-ops when today's jokes exist or retries are exhausted).
scheduler.add_job(
tick,
IntervalTrigger(minutes=settings.retry_minutes),
id="retry-generation",
)
scheduler.start()
yield
scheduler.shutdown(wait=False)
app = FastAPI(title="Wiki Jokes", lifespan=lifespan)
@app.get("/", response_class=HTMLResponse)
def index():
today = today_local()
batch = db.get_day(today)
stale = False
if batch is None:
batch = db.get_latest()
stale = True
if batch is None:
return HTMLResponse(
"<h1>Wiki Jokes</h1><p>No jokes yet — the daily generator is "
"working on it. Check back soon!</p>",
status_code=503,
)
items = "\n".join(
f' <li class="joke">{j}</li>' for j in batch["jokes"]
)
stale_note = (
'<p class="stale">⚠️ Today\'s jokes are still being prepared — '
"showing the latest available batch.</p>"
if stale
else ""
)
return PAGE_TEMPLATE.format(
day=batch["day"],
stale_note=stale_note,
jokes=items,
article_title=batch["article_title"],
article_url=batch["article_url"],
)
@app.get("/health")
def health():
today = today_local()
has_today = db.get_day(today) is not None
return JSONResponse(
{
"status": "ok",
"today": today.isoformat(),
"has_todays_jokes": has_today,
"retries_exhausted": generation_exhausted(today),
}
)