{"record":{"id":"c9bcc44a2590f11f","repo":"larksuite/cli","slug":"invalid-a1-range-endpoint-endpoint","errorCode":null,"errorMessage":"Invalid A1 range endpoint: {endpoint}","messagePattern":"Invalid A1 range endpoint: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/lark-sheets/scripts/lark_sheet_range.py","lineNumber":72,"sourceCode":"    match = CELL_RE.match(cell_ref.strip())\n    if not match:\n        raise ValueError(f\"Invalid cell reference: {cell_ref}\")\n    col, row = match.groups()\n    return int(row), col_to_index(col)\n\n\ndef _parse_endpoint(endpoint: str) -> tuple[str, int, int] | tuple[str, int]:\n    cell = CELL_RE.match(endpoint)\n    if cell:\n        col, row = cell.groups()\n        return \"cell\", int(row), col_to_index(col)\n    row = ROW_RE.match(endpoint)\n    if row:\n        return \"row\", int(row.group(1))\n    column = COLUMN_RE.match(endpoint)\n    if column:\n        return \"column\", col_to_index(column.group(1))\n    raise ValueError(f\"Invalid A1 range endpoint: {endpoint}\")\n\n\ndef parse_range(\n    range_ref: str,\n    *,\n    max_row: int | None = None,\n    max_col: int | None = None,\n) -> RangeBounds:\n    \"\"\"Parse the A1 range forms accepted by ``+csv-get``.\n\n    Open-ended forms need the caller's actual returned grid dimensions. This\n    keeps generated ranges finite without guessing a spreadsheet-wide limit.\n    \"\"\"\n    ref = range_ref.strip()\n    if \"!\" in ref:\n        _, ref = ref.rsplit(\"!\", 1)\n    if not ref:\n        raise ValueError(f\"Invalid A1 range: {range_ref}\")","sourceCodeStart":54,"sourceCodeEnd":90,"githubUrl":"https://github.com/larksuite/cli/blob/7fd6ef3c07182257ce776cdc5a614e122d5bd4b3/skills/lark-sheets/scripts/lark_sheet_range.py#L54-L90","documentation":"_parse_endpoint() parses one endpoint of an A1 range as a cell, row, or column, raising ValueError when the endpoint matches none of CELL_RE, ROW_RE, or COLUMN_RE. It is an internal helper, so this error surfaces indirectly via parse_range when one side of a range string is malformed.","triggerScenarios":"parse_range called with a range whose start or end endpoint is invalid: 'A1:B2:C' (extra split segment reaches _parse_endpoint as 'C'-adjacent garbage only via parts[0]/parts[-1]... actually multi-colon is rejected earlier), 'A1:B2:' (empty end endpoint), ':B2' (empty start), 'A1:2B', 'A1:A0', 'A1:Sheet1!B2' (only the last '!' segment is kept so 'A1' before '!' is dropped — but refs like 'A1:B!2' leave '!2' as end), or endpoints with stray characters like 'A 1' or 'A1 '.","commonSituations":"Copy-pasted ranges with trailing colons or full-width colons (：); ranges that still contain a sheet name attached to the end endpoint; typos like '1A' instead of 'A1'; locale-formatted refs with spaces; programmatically built ranges where a variable was empty or None converted to 'None'.","solutions":["Print/inspect the exact range_ref; fix the endpoint that fails ^\\$?(\\$?[A-Za-z]+\\$?[1-9][0-9]*|[1-9][0-9]*|[A-Za-z]+)$ — e.g. 'A1:2B' → 'A1:B2'.","Remove trailing colons and use ASCII ':' (not '：') between endpoints; drop empty segments ('A1:' → 'A1').","Strip any 'SheetName!' prefix from the whole ref before calling parse_range, since rsplit('!') keeps only the segment after the last '!'.","Fix zero-indexed endpoints ('A1:A0' → 'A1:A1') and correct swapped refs ('1A' → 'A1')."],"exampleFix":"// before\nparse_range(\"A1:2B\")     # Invalid A1 range endpoint: 2B\nparse_range(\"A1:\")       # Invalid A1 range endpoint: (empty)\nparse_range(\"Sheet1!A1:B2\")  # sheet part silently handled, but \"A1:B!2\" fails\n// after\nparse_range(\"A1:B2\")\nparse_range(\"A1\")\nref = raw.split(\"!\")[-1]  # strip sheet prefix first\nparse_range(ref)","handlingStrategy":"validation","validationCode":"import re\nENDPOINT_RE = re.compile(r\"^\\$?([A-Za-z]+\\$?[1-9][0-9]*|[1-9][0-9]*|[A-Za-z]+)$\")\ndef valid_range(ref: str) -> bool:\n    ref = ref.strip().rsplit(\"!\", 1)[-1]\n    if not ref:\n        return False\n    parts = ref.split(\":\")\n    return len(parts) <= 2 and all(p and ENDPOINT_RE.match(p) for p in parts)","typeGuard":"import re\n_END = re.compile(r\"^\\$?([A-Za-z]+\\$?[1-9][0-9]*|[1-9][0-9]*|[A-Za-z]+)$\")\ndef parseable_range(ref: str) -> str | None:\n    r = ref.strip().rsplit(\"!\", 1)[-1]\n    parts = r.split(\":\")\n    if len(parts) > 2 or not all(p and _END.match(p) for p in parts):\n        return None\n    return r","tryCatchPattern":"from lark_sheet_range import parse_range\ntry:\n    bounds = parse_range(range_ref)\nexcept ValueError as e:\n    raise SystemExit(f\"--range {range_ref!r} is not valid A1 notation: {e}\")","preventionTips":["Normalize input first: strip whitespace, replace full-width '：' with ':', strip 'Sheet!' prefixes.","Build ranges programmatically as f\"{start}:{end}\" with non-empty, validated endpoints.","Avoid pasting URLs or UI strings directly; extract only the A1 portion.","Remember row parts must start at 1 (no '0') and column parts are letters only."],"tags":["python","input-validation","a1-notation","valueerror"],"backgroundTag":"invalid-range-notation","analyzedSha":"7fd6ef3c07182257ce776cdc5a614e122d5bd4b3","analyzedAt":"2026-09-04T21:17:44.649Z","contentChangedAt":"2026-09-04T21:17:44.649Z","schemaVersion":2},"datasetVersion":"2026-09-12T02:17:10.037Z"}