Imposible Parse Range
Describe the bug When executing a batch_update in a Worksheet the parsed range explodes
To Reproduce To be honest, I don't know why it happens. And it rarely happens without even changing the code. I suspect it's more on the side of the Google API rather than on GSpread.
Although the example I show is related to GitHub Actions, in reality it has happened to me before both with local environment as well as with Google Colab.
Expected behavior In theory, it just executes the batch update, but in practice this is the error it generates:
APIError: APIError: [400]: Invalid data[0]: Unable to parse range: 'Funnel'!'Funnel'!'Funnel'!'Funnel'!A27122:AX27132
Code example*
def _batch_update_rows(ws, start_col_letter: str, end_col_letter: str, row_blocks: list[tuple[int,int,list[list[str]]]], cell_threshold: int = 10000):
"""
Updates Google Sheets using batch_update to minimize API calls.
Groups row_blocks into 'mega-batches' based on a cell_threshold.
"""
current_batch_data = []
current_cell_count = 0
for (r1, r2, mat) in row_blocks:
# Calculate cells in this specific block
block_cells = len(mat) * len(mat[0]) if mat else 0
rng = f"{start_col_letter}{r1}:{end_col_letter}{r2}"
# Prepare the update object for this block
update_item = {
'range': rng,
'values': mat
}
# Check if adding this block exceeds our threshold
if current_cell_count + block_cells > cell_threshold and current_batch_data:
# Execute the accumulated batch before starting a new one
_execute_batch_retry(ws, current_batch_data)
current_batch_data = []
current_cell_count = 0
sleep(0.5) # Slight breather between mega-batches
current_batch_data.append(update_item)
current_cell_count += block_cells
# Final execution for any remaining data
if current_batch_data:
_execute_batch_retry(ws, current_batch_data)
def _execute_batch_retry(ws, data_list):
"""
Helper to wrap the batch_update in your retry logic.
"""
_retry(
lambda: ws.batch_update(data_list, value_input_option="USER_ENTERED"),
label=f"batch_update for {len(data_list)} ranges"
)
# Funcion auxiliar para unir filas actualizadas en una sola y poder realizar cambios enteros por chunks
def _make_consecutive_blocks(rownums_sorted: list[int], values_by_rownum: dict[int, list[str]]):
"""
Agrupa filas consecutivas para reducir llamadas a la API.
Retorna [(start_row, end_row, matrix_values)]
"""
blocks = []
if not rownums_sorted:
return blocks
start = prev = rownums_sorted[0]
mat = [values_by_rownum[start]]
for r in rownums_sorted[1:]:
# Si la fila es adyacente a la anterior se uno como un bloque
if r == prev + 1:
mat.append(values_by_rownum[r])
prev = r
else:
# Si no, entonces se guarda el bloque y se crea uno nuevo
blocks.append((start, prev, mat))
start = prev = r
mat = [values_by_rownum[r]]
# Se guarda el último bloque en memoria
blocks.append((start, prev, mat))
return blocksScreenshots
Environment info:
- Operating System [e.g. Linux, Windows, macOS]: Ubuntu Linux (2.336.0)
- Python version: 3.11.15
- gspread version: 6.2.1
Stack trace or other output that would be helpful Cell In[39], line 60, in _batch_update_rows(ws, start_col_letter, end_col_letter, row_blocks, cell_threshold) 56 57 # Check if adding this block exceeds our threshold 58 if current_cell_count + block_cells > cell_threshold and current_batch_data: 59 # Execute the accumulated batch before starting a new one ---> 60 _execute_batch_retry(ws, current_batch_data) 61 current_batch_data = [] 62 current_cell_count = 0 63 sleep(0.5) # Slight breather between mega-batches
Cell In[39], line 76, in _execute_batch_retry(ws, data_list) 72 def _execute_batch_retry(ws, data_list): 73 """ 74 Helper to wrap the batch_update in your retry logic. 75 """ ---> 76 _retry( 77 lambda: ws.batch_update(data_list, value_input_option="USER_ENTERED"), 78 label=f"batch_update for len(data_list) ranges" 79 )
Cell In[3], line 57, in _retry(fn, label, tries, base_sleep, jitter, max_sleep) 53 print(f"[RETRY i+1/tries] label -> msg[:120]... sleep sleep_s:.1fs") 54 # Esperamos para no saturar al API 55 sleep(sleep_s) 56 continue ---> 57 raise e 58 raise last_err
Cell In[3], line 57, in _retry(fn, label, tries, base_sleep, jitter, max_sleep) 53 print(f"[RETRY i+1/tries] label -> msg[:120]... sleep sleep_s:.1fs") 54 # Esperamos para no saturar al API 55 sleep(sleep_s) 56 continue ---> 57 raise e 58 raise last_err
Cell In[39], line 77, in _execute_batch_retry..() ---> 77 lambda: ws.batch_update(data_list, value_input_option="USER_ENTERED"),
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/gspread/worksheet.py:1368, in Worksheet.batch_update(self, data, raw, value_input_option, include_values_in_response, response_value_render_option, response_date_time_render_option) 1358 values["range"] = absolute_range_name(self.title, values["range"]) 1360 body: MutableMapping[str, Any] = *** 1361 "valueInputOption": value_input_option, 1362 "includeValuesInResponse": include_values_in_response, (...) 1365 "data": data, 1366 *** -> 1368 response = self.client.values_batch_update(self.spreadsheet_id, body=body) 1370 return response
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/gspread/http_client.py:268, in HTTPClient.values_batch_update(self, id, body)
261 """Lower-level method that directly calls spreadsheets/<ID>/values:batchUpdate <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate>.
262
263 :param dict body: (optional) Values Batch Update Request body <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate#request-body>.
264 :returns: Values Batch Update Response body <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/batchUpdate#response-body>_.
265 :rtype: dict
266 """
267 url = SPREADSHEET_VALUES_BATCH_UPDATE_URL % id
--> 268 r = self.request("post", url, json=body)
269 return r.json()
File /opt/hostedtoolcache/Python/3.11.15/x64/lib/python3.11/site-packages/gspread/http_client.py:128, in HTTPClient.request(self, method, endpoint, params, data, json, files, headers) 126 return response 127 else: --> 128 raise APIError(response)
APIError: APIError: [400]: Invalid data[0]: Unable to parse range: 'Funnel'!'Funnel'!'Funnel'!'Funnel'!A27122:AX27132
Source: burnash/gspread