This commit is contained in:
Taylor Wilsdon
2026-03-15 17:52:13 -04:00
parent 042d2190e9
commit bf1f94b330
2 changed files with 104 additions and 75 deletions

View File

@@ -7,11 +7,15 @@ conditional formatting helpers.
import asyncio import asyncio
import json import json
import logging
import re import re
from typing import List, Optional, Union from typing import List, Optional, Union
from core.utils import UserInputError from core.utils import UserInputError
logger = logging.getLogger(__name__)
MAX_GRID_METADATA_CELLS = 5000
A1_PART_REGEX = re.compile(r"^([A-Za-z]*)(\d*)$") A1_PART_REGEX = re.compile(r"^([A-Za-z]*)(\d*)$")
SHEET_TITLE_SAFE_RE = re.compile(r"^[A-Za-z0-9_]+$") SHEET_TITLE_SAFE_RE = re.compile(r"^[A-Za-z0-9_]+$")
@@ -955,3 +959,94 @@ def _format_sheet_notes_section(
else "" else ""
) )
return f"\n\nCell notes in range '{range_label}':\n" + "\n".join(lines) + suffix return f"\n\nCell notes in range '{range_label}':\n" + "\n".join(lines) + suffix
async def _fetch_grid_metadata(
service,
spreadsheet_id: str,
resolved_range: str,
values: List[List[object]],
include_hyperlinks: bool = False,
include_notes: bool = False,
) -> tuple[str, str]:
"""Fetch hyperlinks and/or notes for a range via a single spreadsheets.get call.
Computes tight range bounds, enforces the cell-count cap, builds a combined
``fields`` selector so only one API round-trip is needed when both flags are
``True``, then parses the response into formatted output sections.
Returns:
(hyperlink_section, notes_section) — each is an empty string when the
corresponding flag is ``False`` or no data was found.
"""
if not include_hyperlinks and not include_notes:
return "", ""
tight_range = _a1_range_for_values(resolved_range, values)
if not tight_range:
logger.info(
"[read_sheet_values] Skipping grid metadata fetch for range '%s': "
"unable to determine tight bounds",
resolved_range,
)
return "", ""
cell_count = _a1_range_cell_count(tight_range) or sum(
len(row) for row in values
)
if cell_count > MAX_GRID_METADATA_CELLS:
logger.info(
"[read_sheet_values] Skipping grid metadata fetch for large range "
"'%s' (%d cells > %d limit)",
tight_range,
cell_count,
MAX_GRID_METADATA_CELLS,
)
return "", ""
# Build a combined fields selector so we hit the API at most once.
value_fields: list[str] = []
if include_hyperlinks:
value_fields.extend(["hyperlink", "textFormatRuns(format(link(uri)))"])
if include_notes:
value_fields.append("note")
fields = (
"sheets(properties(title),data(startRow,startColumn,"
f"rowData(values({','.join(value_fields)}))))"
)
try:
response = await asyncio.to_thread(
service.spreadsheets()
.get(
spreadsheetId=spreadsheet_id,
ranges=[tight_range],
includeGridData=True,
fields=fields,
)
.execute
)
except Exception as exc:
logger.warning(
"[read_sheet_values] Failed fetching grid metadata for range '%s': %s",
tight_range,
exc,
)
return "", ""
hyperlink_section = ""
if include_hyperlinks:
hyperlinks = _extract_cell_hyperlinks_from_grid(response)
hyperlink_section = _format_sheet_hyperlink_section(
hyperlinks=hyperlinks, range_label=tight_range
)
notes_section = ""
if include_notes:
notes = _extract_cell_notes_from_grid(response)
notes_section = _format_sheet_notes_section(
notes=notes, range_label=tight_range
)
return hyperlink_section, notes_section

View File

@@ -15,18 +15,14 @@ from core.server import server
from core.utils import handle_http_errors, UserInputError from core.utils import handle_http_errors, UserInputError
from core.comments import create_comment_tools from core.comments import create_comment_tools
from gsheets.sheets_helpers import ( from gsheets.sheets_helpers import (
_a1_range_cell_count,
CONDITION_TYPES, CONDITION_TYPES,
_a1_range_for_values, _a1_range_for_values,
_build_boolean_rule, _build_boolean_rule,
_build_gradient_rule, _build_gradient_rule,
_fetch_detailed_sheet_errors, _fetch_detailed_sheet_errors,
_fetch_sheet_hyperlinks, _fetch_grid_metadata,
_fetch_sheet_notes,
_fetch_sheets_with_rules, _fetch_sheets_with_rules,
_format_conditional_rules_section, _format_conditional_rules_section,
_format_sheet_hyperlink_section,
_format_sheet_notes_section,
_format_sheet_error_section, _format_sheet_error_section,
_parse_a1_range, _parse_a1_range,
_parse_condition_values, _parse_condition_values,
@@ -38,7 +34,6 @@ from gsheets.sheets_helpers import (
# Configure module logger # Configure module logger
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_HYPERLINK_FETCH_CELLS = 5000
@server.tool() @server.tool()
@@ -216,75 +211,14 @@ async def read_sheet_values(
resolved_range = result.get("range", range_name) resolved_range = result.get("range", range_name)
detailed_range = _a1_range_for_values(resolved_range, values) or resolved_range detailed_range = _a1_range_for_values(resolved_range, values) or resolved_range
hyperlink_section = "" hyperlink_section, notes_section = await _fetch_grid_metadata(
if include_hyperlinks: service,
# Use a tight A1 range for includeGridData fetches to avoid expensive spreadsheet_id,
# open-ended requests (e.g., A:Z). resolved_range,
hyperlink_range = _a1_range_for_values(resolved_range, values) values,
if not hyperlink_range: include_hyperlinks=include_hyperlinks,
logger.info( include_notes=include_notes,
"[read_sheet_values] Skipping hyperlink fetch for range '%s': unable to determine tight bounds", )
resolved_range,
)
else:
cell_count = _a1_range_cell_count(hyperlink_range) or sum(
len(row) for row in values
)
if cell_count <= MAX_HYPERLINK_FETCH_CELLS:
try:
hyperlinks = await _fetch_sheet_hyperlinks(
service, spreadsheet_id, hyperlink_range
)
hyperlink_section = _format_sheet_hyperlink_section(
hyperlinks=hyperlinks, range_label=hyperlink_range
)
except Exception as exc:
logger.warning(
"[read_sheet_values] Failed fetching hyperlinks for range '%s': %s",
hyperlink_range,
exc,
)
else:
logger.info(
"[read_sheet_values] Skipping hyperlink fetch for large range '%s' (%d cells > %d limit)",
hyperlink_range,
cell_count,
MAX_HYPERLINK_FETCH_CELLS,
)
notes_section = ""
if include_notes:
notes_range = _a1_range_for_values(resolved_range, values)
if not notes_range:
logger.info(
"[read_sheet_values] Skipping notes fetch for range '%s': unable to determine tight bounds",
resolved_range,
)
else:
cell_count = _a1_range_cell_count(notes_range) or sum(
len(row) for row in values
)
if cell_count <= MAX_HYPERLINK_FETCH_CELLS:
try:
notes = await _fetch_sheet_notes(
service, spreadsheet_id, notes_range
)
notes_section = _format_sheet_notes_section(
notes=notes, range_label=notes_range
)
except Exception as exc:
logger.warning(
"[read_sheet_values] Failed fetching notes for range '%s': %s",
notes_range,
exc,
)
else:
logger.info(
"[read_sheet_values] Skipping notes fetch for large range '%s' (%d cells > %d limit)",
notes_range,
cell_count,
MAX_HYPERLINK_FETCH_CELLS,
)
detailed_errors_section = "" detailed_errors_section = ""
if _values_contain_sheets_errors(values): if _values_contain_sheets_errors(values):