666ghj/MiroFish · error · StarHistoryError

{label} must use YYYY-MM-DDTHH:MM:SSZ

Error message

{label} must use YYYY-MM-DDTHH:MM:SSZ

What it means

_parse_state_timestamp validates persisted-state timestamps against STATE_TIMESTAMP_RE (r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$') and raises StarHistoryError(f'{label} must use YYYY-MM-DDTHH:MM:SSZ') when the value is not a string or does not fullmatch. This guards the history-state JSON file against format drift (fractional seconds, offsets, missing Z) before strptime runs.

Source

Thrown at scripts/star_history.py:319

def _strict_non_negative_int(value: Any, label: str) -> int:
    if type(value) is not int or value < 0:
        raise StarHistoryError(f"{label} must be a non-negative integer")
    return value


def _parse_github_timestamp(value: str) -> datetime:
    try:
        parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
    except ValueError as exc:
        raise StarHistoryError("GitHub returned an invalid star timestamp") from exc
    if parsed.tzinfo is None or parsed.utcoffset() != timedelta(0):
        raise StarHistoryError("GitHub star timestamp was not UTC")
    return parsed.astimezone(UTC)


def _parse_state_timestamp(value: Any, label: str) -> datetime:
    if not isinstance(value, str) or not STATE_TIMESTAMP_RE.fullmatch(value):
        raise StarHistoryError(f"{label} must use YYYY-MM-DDTHH:MM:SSZ")
    try:
        parsed = datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC)
    except ValueError as exc:
        raise StarHistoryError(f"{label} is not a valid UTC timestamp") from exc
    return parsed


def _format_state_timestamp(value: datetime) -> str:
    normalized = _normalize_now(value)
    return normalized.strftime("%Y-%m-%dT%H:%M:%SZ")


def _normalize_now(value: datetime) -> datetime:
    if value.tzinfo is None or value.utcoffset() != timedelta(0):
        raise StarHistoryError("clock must return a UTC datetime")
    return value.astimezone(UTC).replace(microsecond=0)

View on GitHub (pinned to b5b53acc57)

Solutions

  1. Reset the state file to let the script regenerate it from scratch (losing incremental history is often acceptable)
  2. Or fix the offending timestamp(s) in the state file to strict 'YYYY-MM-DDTHH:MM:SSZ' form
  3. Identify the writer that produced the bad format and route it through _format_state_timestamp

Example fix

// before (state file)
"generated_at": "2024-01-15T10:30:00.123456+00:00"

// after (state file)
"generated_at": "2024-01-15T10:30:00Z"
Defensive patterns

Strategy: validation

Validate before calling

import re
STATE_TIMESTAMP_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$")

bad = [k for k, v in state_flat_timestamps.items() if not (isinstance(v, str) and STATE_TIMESTAMP_RE.fullmatch(v))]
if bad:
    raise StarHistoryError(f"timestamps not in YYYY-MM-DDTHH:MM:SSZ: {bad}")

Type guard

def is_state_timestamp(value: object) -> TypeGuard[str]:
    return isinstance(value, str) and bool(STATE_TIMESTAMP_RE.fullmatch(value))

Try / catch

try:
    validate_state(state)
except StarHistoryError as exc:
    if "must use YYYY-MM-DDTHH:MM:SSZ" in str(exc):
        regenerate_state_file()
    else:
        raise

Prevention

When it happens

Trigger: A state file containing 'generated_at': '2024-01-15T10:30:00' (no Z), '...:00.123456Z' (fractional seconds), '...+00:00' (offset instead of Z), or a non-string; also hand-edited state files.

Common situations: State file written by an older/newer version of the script with a different timestamp format; someone edited the JSON by hand and pasted a timezone-offset timestamp; a different tool regenerated the file with fromisoformat-style output.

Related errors


AI-assisted analysis of 666ghj/MiroFish@b5b53acc57 (2026-08-14). Data as JSON: /api/errors/222cb51b1cd49174. Report an issue: GitHub.