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:
+111
@@ -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),
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user