JuliusBrussee/caveman · error · CatalogError

: verified_at must include a timezone

Error message

{label}: verified_at must include a timezone

What it means

verified_at parsed as a datetime/string but carries no timezone (naive datetime). The validator requires an explicit timezone so every row's date converts deterministically to UTC — provenance dates must be unambiguous across timezones. Thrown from parsed_time after successful parsing.

Solutions

  1. Append `Z` (or `+00:00`) to the timestamp: `2026-07-05T00:00:00Z`
  2. If the value is a local time, convert to UTC first, then add the offset
  3. Generate dates with timezone-aware code: `datetime.now(timezone.utc).isoformat()`

Example fix

# before
verified_at: "2026-07-05T00:00:00"

# after
verified_at: "2026-07-05T00:00:00Z"
Defensive patterns

Strategy: validation

Validate before calling

from datetime import datetime

def has_timezone(value: str) -> bool:
    try:
        return datetime.fromisoformat(value.replace("Z", "+00:00")).tzinfo is not None
    except ValueError:
        return False

Type guard

def is_aware_timestamp(value: object) -> bool:
    if isinstance(value, datetime):
        return value.tzinfo is not None
    return isinstance(value, str) and has_timezone(value)

Try / catch

try:
    validate_catalog()
except CatalogError as e:
    if "must include a timezone" in str(e):
        append_utc_suffix(str(e))

Prevention

When it happens

Trigger: verified_at is a naive ISO string like `2026-07-05T00:00:00` (no Z or offset), or a quoted YAML date `"2026-07-05"`-style value parsed without tz info, or a YAML unquoted timestamp lacking an offset in a form PyYAML parses as naive.

Common situations: Writing `verified_at: 2026-07-05T12:00:00` and assuming UTC; local tooling generating naive isoformat() output (`datetime.now().isoformat()` instead of `datetime.now(timezone.utc).isoformat()`).

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/503f15e25cbc7969. Report an issue: GitHub.

Appendix: source

Thrown at shared/provider-catalog/validate_catalog.py:112

    if not isinstance(document, list) or not document:
        raise CatalogError(f"{path}: catalog must be a non-empty list")
    if not all(isinstance(row, dict) for row in document):
        raise CatalogError(f"{path}: every catalog row must be an object")
    return document


def parsed_time(value: Any, label: str) -> datetime:
    if isinstance(value, datetime):
        parsed = value
    elif isinstance(value, str):
        try:
            parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
        except ValueError as error:
            raise CatalogError(f"{label}: verified_at must be RFC3339") from error
    else:
        raise CatalogError(f"{label}: verified_at must be an RFC3339 timestamp")
    if parsed.tzinfo is None:
        raise CatalogError(f"{label}: verified_at must include a timezone")
    return parsed.astimezone(timezone.utc)


def validate_row(row: dict[str, Any], label: str, now: datetime) -> tuple[str, str, str]:
    unknown = set(row) - TOP_LEVEL_KEYS
    missing = REQUIRED_KEYS - set(row)
    if unknown:
        raise CatalogError(f"{label}: unknown field(s): {', '.join(sorted(unknown))}")
    if missing:
        raise CatalogError(f"{label}: missing field(s): {', '.join(sorted(missing))}")

    identity: list[str] = []
    for field in ("provider", "model", "region"):
        value = row[field]
        if not isinstance(value, str) or not IDENTIFIER.fullmatch(value):
            raise CatalogError(f"{label}: {field} must be a safe non-empty identifier")
        identity.append(value)
    if row["currency"] != "USD":

View on GitHub (pinned to 3ee70a1026)