{"record":{"id":"82abdd71f2d9c101","repo":"MemPalace/mempalace","slug":"field-name-must-be-an-iso-date-string-e-g-202","errorCode":null,"errorMessage":"{field_name} must be an ISO date string (e.g. '2026-04-01' or '2026-04-01T09:30:00'), got {value!r}","messagePattern":"(.+?) must be an ISO date string \\(e\\.g\\. '2026-04-01' or '2026-04-01T09:30:00'\\), got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"mempalace/date_window.py","lineNumber":62,"sourceCode":"    clear error, mirroring the wing/room sanitizers.\n    \"\"\"\n    if value is None:\n        return None\n    if not isinstance(value, str):\n        raise ValueError(f\"{field_name} must be an ISO date string\")\n    value = value.strip()\n    if not value:\n        return None\n    # datetime.fromisoformat before Python 3.11 rejects a trailing \"Z\" (Zulu),\n    # and appending \"+00:00\" would break a date-only value on 3.9/3.10\n    # (\"2026-04-01+00:00\" is rejected there). Any offset is dropped below for\n    # wall-clock comparison anyway, so just strip a trailing Z/z; both date and\n    # date-time Zulu inputs then parse on the 3.9 floor.\n    iso = value[:-1] if value.endswith((\"Z\", \"z\")) else value\n    try:\n        parsed = datetime.fromisoformat(iso)\n    except ValueError as exc:\n        raise ValueError(\n            f\"{field_name} must be an ISO date string \"\n            f\"(e.g. '2026-04-01' or '2026-04-01T09:30:00'), got {value!r}\"\n        ) from exc\n    if parsed.tzinfo is not None:\n        parsed = parsed.replace(tzinfo=None)\n    return parsed\n\n\ndef parse_window(since: Optional[str] = None, before: Optional[str] = None):\n    \"\"\"Parse a ``[since, before)`` pair, rejecting an inverted window.\n\n    Returns ``(since_dt, before_dt)`` — either side ``None`` when absent.\n    Raises ``ValueError`` (naming the offending field or the inversion) so\n    callers surface the same message everywhere a window is accepted.\n    \"\"\"\n    since_dt = parse_date_bound(since, \"since\")\n    before_dt = parse_date_bound(before, \"before\")\n    if since_dt is not None and before_dt is not None and since_dt >= before_dt:","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/MemPalace/mempalace/blob/06cb6987f02610784fefbad4b2bd5d026d164ba6/mempalace/date_window.py#L44-L80","documentation":"ValueError from parse_date_bound (mempalace/date_window.py:62): the value is a string but datetime.fromisoformat cannot parse it after stripping an optional trailing Z/z. The message shows the field name, the expected formats, and the offending value verbatim. The accepted grammar is narrow by design: a date, an ISO timestamp (optional fractional seconds), optional Z or ±HH:MM offset — basic-format (no dashes), week dates, and other ISO 8601 exotica are rejected on the Python 3.9 floor.","triggerScenarios":"Passing '2026/04/01' (slashes), '20260401' (basic format), '2026-W14-2' (week date), 'Apr 1 2026', or any locale-formatted date; timestamps with a space separator instead of 'T' where fromisoformat rejects them; stray characters or double Z.","commonSituations":"User-supplied date strings from chat/CLI entering search filters; data exported from spreadsheets with regional formats; assuming full ISO 8601 support when the contract is the extended format subset only.","solutions":["Use extended ISO format: 'YYYY-MM-DD' or 'YYYY-MM-DDTHH:MM:SS' (fractional seconds and a Z or ±HH:MM offset allowed).","Normalize user input before calling: parse with dateutil or strptime using the actual input format, then pass .isoformat().","Check the got {value!r} portion of the message to spot invisible characters or wrong separators."],"exampleFix":"# before\nsearch(since=\"2026/04/01\")\n\n# after\nfrom datetime import datetime\nsearch(since=datetime.strptime(\"2026/04/01\", \"%Y/%m/%d\").date().isoformat())","handlingStrategy":"validation","validationCode":"from datetime import datetime\n\ndef normalize_iso(value: str) -> str:\n    v = value.strip()\n    if v.endswith((\"Z\", \"z\")):\n        v = v[:-1]\n    datetime.fromisoformat(v)  # raises with context if unparseable\n    return v","typeGuard":"def is_iso_date_str(value: object) -> bool:\n    if not isinstance(value, str) or not value.strip():\n        return False\n    try:\n        datetime.fromisoformat(value.strip().rstrip(\"Zz\"))\n        return True\n    except ValueError:\n        return False","tryCatchPattern":"try:\n    parse_window(since, before)\nexcept ValueError as exc:\n    # user-facing message already includes field name, expected format, and got-value\n    return error_response(str(exc))","preventionTips":["Accept only extended ISO format ('YYYY-MM-DD' / 'YYYY-MM-DDTHH:MM:SS'); reject basic format and week dates by design.","Parse locale-formatted user input with strptime/dateutil first, then pass .isoformat().","Log the repr of rejected values to catch invisible whitespace/encoding issues."],"tags":["validation","dates","parsing","iso8601"],"backgroundTag":null,"analyzedSha":"06cb6987f02610784fefbad4b2bd5d026d164ba6","analyzedAt":"2026-08-15T03:03:36.213Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}