From d84064ba84abd14096e1c1540d6779614ceb7400 Mon Sep 17 00:00:00 2001 From: liuji Date: Sat, 10 Oct 2026 15:08:53 +0800 Subject: [PATCH] Read gviz column labels as headers --- sync.py | 52 ++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/sync.py b/sync.py index 27e2664..f6f9b40 100755 --- a/sync.py +++ b/sync.py @@ -39,7 +39,7 @@ def sheet_names(xlsx: bytes) -> list[str]: def read_gviz(sheet: str) -> list[list[str]]: - params = urlencode({"sheet": sheet, "headers": "0"}) + params = urlencode({"sheet": sheet}) url = f"https://docs.google.com/spreadsheets/d/{WORKBOOK_ID}/gviz/tq?{params}" response = requests.get(url, timeout=30) response.raise_for_status() @@ -50,32 +50,56 @@ def read_gviz(sheet: str) -> list[list[str]]: text = text[:-1].rstrip() if not text.startswith(GVIZ_PREFIX) or not text.endswith(")"): raise ValueError(f"invalid gviz response for worksheet {sheet!r}") - payload = json.loads(text[len(GVIZ_PREFIX) : -1]) - if payload.get("status") != "ok": + try: + payload = json.loads(text[len(GVIZ_PREFIX) : -1]) + except json.JSONDecodeError as error: + raise ValueError(f"invalid gviz response for worksheet {sheet!r}") from error + if not isinstance(payload, dict) or payload.get("status") != "ok": raise ValueError(f"gviz query failed for worksheet {sheet!r}") - table = payload.get("table", {}) - columns = table.get("cols", []) - rows = table.get("rows", []) - width = len(columns) + table = payload.get("table") + if not isinstance(table, dict): + raise ValueError(f"invalid gviz table for worksheet {sheet!r}") + columns = table.get("cols") + rows = table.get("rows") + if not isinstance(columns, list) or not isinstance(rows, list): + raise ValueError(f"invalid gviz table for worksheet {sheet!r}") + + headers: list[str] = [] + for column in columns: + if not isinstance(column, dict): + raise ValueError(f"invalid gviz column for worksheet {sheet!r}") + label = column.get("label") + if label is not None and not isinstance(label, str): + raise ValueError(f"invalid gviz column label for worksheet {sheet!r}") + headers.append(label or "") + + while headers and headers[-1] == "": + headers.pop() + if not headers: + return [] def cell_value(cell: dict | None) -> str: - if not cell: + if cell is None: return "" - value = cell.get("f") if cell.get("f") is not None else cell.get("v", "") + if not isinstance(cell, dict): + raise ValueError(f"invalid gviz cell for worksheet {sheet!r}") + value = cell.get("f") if cell.get("f") is not None else cell.get("v") if value is None: return "" if isinstance(value, bool): return "TRUE" if value else "FALSE" return str(value) - result: list[list[str]] = [] + data_rows: list[list[str]] = [] for row in rows: - cells = row.get("c", []) - if len(cells) > width: + if not isinstance(row, dict) or not isinstance(row.get("c", []), list): + raise ValueError(f"invalid gviz row for worksheet {sheet!r}") + cells = row["c"] + if len(cells) > len(columns): raise ValueError(f"gviz row is wider than columns in worksheet {sheet!r}") values = [cell_value(cell) for cell in cells] - result.append(values + [""] * (width - len(values))) - return result + data_rows.append((values + [""] * (len(columns) - len(values)))[: len(headers)]) + return [headers] + data_rows def normalize_sheet(name: str, rows: list[list[str]]) -> tuple[list[str], list[list[str]]]: