paperclipai/paperclip · error · ValueError
History date must include a timezone
Error message
History date must include a timezone
What it means
display_date() parses an ISO-8601 timestamp with datetime.fromisoformat and requires it to carry timezone information so it can convert to UTC for display. A naive datetime (no offset/Z) would make the rendered date ambiguous, so the script raises ValueError.
Solutions
- Fix the producer to serialize timezone-aware ISO timestamps (e.g. datetime.now(timezone.utc).isoformat(), which includes +00:00).
- If the source is known-UTC, append an offset before parsing: value + "+00:00" when no Z/offset is present.
- In fixtures, write "2026-09-18T12:00:00Z" or "+00:00"-suffixed timestamps.
- If naive dates must be tolerated, decide a policy (assume UTC) and normalize in display_date.
Example fix
# before measured = "2026-09-18T12:00:00" # naive # after measured = "2026-09-18T12:00:00Z" # or +00:00 suffix
Defensive patterns
Strategy: validation
Validate before calling
from datetime import datetime
def is_aware_iso(value):
d = datetime.fromisoformat(value.replace("Z", "+00:00"))
return d.tzinfo is not None Type guard
def has_timezone(value):
try:
return datetime.fromisoformat(value.replace("Z", "+00:00")).tzinfo is not None
except ValueError:
return False Try / catch
try:
values = summarize(history, kind)
except ValueError as e:
if str(e) == "History date must include a timezone":
history = normalize_dates_to_utc(history) # append +00:00 to naive stamps
values = summarize(history, kind)
else:
raise Prevention
- Serialize all feed timestamps with timezone info: datetime.now(timezone.utc).isoformat().
- Always suffix UTC stamps with Z or +00:00 in fixtures and hand-edited data.
- Validate timestamp fields on the producer side before publishing history.
- Never use naive datetime.now() when writing campaign/revision timestamps.
When it happens
Trigger: Calling display_date (via summarize or the refreshed timestamp) with a string like "2026-09-18T12:00:00" — no 'Z' suffix and no UTC offset. Note the code only replaces a trailing 'Z' with '+00:00'; strings with neither Z nor offset stay naive and trigger the error.
Common situations: History feeds written by a producer that serialized naive local datetimes; hand-edited fixture dates; dates generated with datetime.now() (naive) instead of datetime.now(timezone.utc).
Related errors
- History has no latest campaign
- Invalid campaign counts
- Latest campaign is absent from history
- Missing campaign coverage flag
- Report refresh has no reliable measurement date
AI-assisted analysis of paperclipai/paperclip@3f1d897a7c (2026-09-18).
Data as JSON: /api/errors/615ad1d2499613cc.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/evals-hub/build.py:46
if not candidates:
raise ValueError("Latest campaign is absent from history")
# Report refreshes replace presentation, never the measurement date.
return max(candidates, key=lambda c: c.get("reportRevision", {}).get("renderedAt", c["generatedAt"]))
def public_report(url, prefix):
parsed = urlsplit(url)
if (parsed.scheme != "https" or parsed.netloc != urlsplit(ORIGIN).netloc
or not parsed.path.startswith(f"/{prefix}/campaigns/")
or parsed.query or parsed.fragment):
raise ValueError("Unexpected public report URL")
return html.escape(url, quote=True)
def display_date(value):
date = datetime.fromisoformat(value.replace("Z", "+00:00"))
if date.tzinfo is None:
raise ValueError("History date must include a timezone")
return date.astimezone(timezone.utc).strftime("%d %b %Y · %H:%M UTC")
def summarize(history, kind):
prefix, schema = SYSTEMS[kind]
campaign = latest(history, schema)
totals = campaign["totals"] if kind == "runner" else campaign
passed, selected = totals["passed"], totals["selected"]
if any(type(n) is not int for n in (passed, selected)) or not 0 <= passed <= selected:
raise ValueError("Invalid campaign counts")
if type(campaign.get("complete")) is not bool:
raise ValueError("Missing campaign coverage flag")
revision = campaign.get("reportRevision", {})
if revision and not isinstance(revision.get("sourceGeneratedAt"), str):
raise ValueError("Report refresh has no reliable measurement date")
measured = revision.get("sourceGeneratedAt", campaign["generatedAt"])
return {
"url": public_report(campaign["publicUrl"], prefix),View on GitHub (pinned to 3f1d897a7c)