Daily Art: Codex guardrails (atomic image, attribution/license, blocked lever)

Hardening before it runs further on the cycle:
- DB-lock/network: all HTTP (metadata + image) happens before any write; the write txn
  opens only at the brief INSERT and commits immediately. Images download to a temp file
  then atomic os.replace into cache (a reader never sees a half-written file).
- Site-timezone "daily" already used local_today() (same rhythm as the Brief) — confirmed.
- Attribution from day one: store + return title/artist/date/medium/department/credit/
  source_url/object_id/source + museum name + is_public_domain license marker + the full-
  res source URL (for a richer /art view later). UI can show: Title · Artist · The Met.
- "highlight != always beautiful": added a manual `blocked` flag on art_pool (excluded
  from picks) as the cheap curation lever; a featured override can follow.

Schema migrated (existing art tables get the new columns). 373 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-21 15:28:38 -04:00
parent 308516a263
commit db967bb7fa
4 changed files with 50 additions and 10 deletions
+19 -6
View File
@@ -92,7 +92,8 @@ def _object(object_id: int) -> dict:
def _download_image(obj: dict, object_id: int) -> str | None:
"""Download the web-large (then full) image to our cache; return the filename or None."""
"""Download the web-large (then full) image to our cache; return the filename or None.
Writes to a temp file then atomically renames, so a reader never sees a half-file."""
for key in ("primaryImageSmall", "primaryImage"):
url = obj.get(key)
if not url:
@@ -105,9 +106,16 @@ def _download_image(obj: dict, object_id: int) -> str | None:
continue
ext = ".png" if "png" in ctype else ".jpg"
fname = f"{object_id}{ext}"
cdir = cache_dir()
tmp = cdir / f".{object_id}.tmp"
try:
(cache_dir() / fname).write_bytes(data)
tmp.write_bytes(data)
os.replace(tmp, cdir / fname) # atomic
except OSError:
try:
tmp.unlink()
except OSError:
pass
return None
return fname
return None
@@ -117,7 +125,8 @@ def _candidates(conn: sqlite3.Connection, art_date: str, source: str) -> list[in
"""The N least-recently-shown pool IDs, rotated deterministically by the date so the
same piece shows for everyone that day and pieces don't repeat soon."""
rows = conn.execute(
"SELECT object_id FROM art_pool WHERE source=? ORDER BY shown_at IS NOT NULL, shown_at, object_id LIMIT ?",
"SELECT object_id FROM art_pool WHERE source=? AND blocked=0 "
"ORDER BY shown_at IS NOT NULL, shown_at, object_id LIMIT ?",
(source, _NO_REPEAT_POOL),
).fetchall()
ids = [r[0] for r in rows]
@@ -146,17 +155,21 @@ def pick_daily(conn: sqlite3.Connection, art_date: str | None = None, source: st
fname = _download_image(obj, oid)
if not fname:
continue
# All network work is done above; only now do we open a brief write txn + commit.
conn.execute(
"INSERT INTO daily_art (art_date, source, object_id, title, artist, date_text, medium, "
"department, credit, source_url, image_file) VALUES (?,?,?,?,?,?,?,?,?,?,?) "
"department, credit, source_url, image_file, image_url_full, is_public_domain) "
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) "
"ON CONFLICT(art_date) DO UPDATE SET object_id=excluded.object_id, title=excluded.title, "
"artist=excluded.artist, date_text=excluded.date_text, medium=excluded.medium, "
"department=excluded.department, credit=excluded.credit, source_url=excluded.source_url, "
"image_file=excluded.image_file",
"image_file=excluded.image_file, image_url_full=excluded.image_url_full, "
"is_public_domain=excluded.is_public_domain",
(art_date, source, oid, obj.get("title") or "Untitled",
obj.get("artistDisplayName") or None, obj.get("objectDate") or None,
obj.get("medium") or None, obj.get("department") or None,
obj.get("creditLine") or None, obj.get("objectURL") or None, fname),
obj.get("creditLine") or None, obj.get("objectURL") or None, fname,
obj.get("primaryImage") or None, 1 if obj.get("isPublicDomain") else 0),
)
conn.execute("UPDATE art_pool SET shown_at=? WHERE source=? AND object_id=?",
(art_date, source, oid))