arcade-google-sheets 2.0.0rc1__py3-none-any.whl → 3.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- arcade_google_sheets/converters.py +108 -0
- arcade_google_sheets/enums.py +124 -0
- arcade_google_sheets/models.py +36 -1
- arcade_google_sheets/templates.py +8 -0
- arcade_google_sheets/tools/__init__.py +17 -3
- arcade_google_sheets/tools/read.py +94 -13
- arcade_google_sheets/tools/search.py +134 -0
- arcade_google_sheets/tools/write.py +123 -11
- arcade_google_sheets/utils.py +476 -1
- {arcade_google_sheets-2.0.0rc1.dist-info → arcade_google_sheets-3.1.0.dist-info}/METADATA +3 -2
- arcade_google_sheets-3.1.0.dist-info/RECORD +18 -0
- arcade_google_sheets-3.1.0.dist-info/licenses/LICENSE +35 -0
- arcade_google_sheets-2.0.0rc1.dist-info/RECORD +0 -15
- arcade_google_sheets-2.0.0rc1.dist-info/licenses/LICENSE +0 -21
- {arcade_google_sheets-2.0.0rc1.dist-info → arcade_google_sheets-3.1.0.dist-info}/WHEEL +0 -0
|
@@ -2,17 +2,20 @@ from typing import Annotated
|
|
|
2
2
|
|
|
3
3
|
from arcade_tdk import ToolContext, tool
|
|
4
4
|
from arcade_tdk.auth import Google
|
|
5
|
-
from arcade_tdk.errors import RetryableToolError
|
|
6
5
|
|
|
6
|
+
from arcade_google_sheets.converters import SheetDataInputToValueRangesConverter
|
|
7
7
|
from arcade_google_sheets.models import (
|
|
8
|
-
SheetDataInput,
|
|
9
8
|
Spreadsheet,
|
|
10
9
|
SpreadsheetProperties,
|
|
11
10
|
)
|
|
12
11
|
from arcade_google_sheets.utils import (
|
|
12
|
+
batch_update,
|
|
13
13
|
build_sheets_service,
|
|
14
|
+
col_to_index,
|
|
14
15
|
create_sheet,
|
|
16
|
+
get_sheet_metadata_from_identifier,
|
|
15
17
|
parse_write_to_cell_response,
|
|
18
|
+
validate_sheet_data_input,
|
|
16
19
|
validate_write_to_cell_params,
|
|
17
20
|
)
|
|
18
21
|
|
|
@@ -40,15 +43,7 @@ def create_spreadsheet(
|
|
|
40
43
|
"""
|
|
41
44
|
service = build_sheets_service(context.get_auth_token_or_empty())
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
sheet_data = SheetDataInput(data=data) # type: ignore[arg-type]
|
|
45
|
-
except Exception as e:
|
|
46
|
-
msg = "Invalid JSON or unexpected data format for parameter `data`"
|
|
47
|
-
raise RetryableToolError(
|
|
48
|
-
message=msg,
|
|
49
|
-
additional_prompt_content=f"{msg}: {e}",
|
|
50
|
-
retry_after_ms=100,
|
|
51
|
-
)
|
|
46
|
+
sheet_data = validate_sheet_data_input(data)
|
|
52
47
|
|
|
53
48
|
spreadsheet = Spreadsheet(
|
|
54
49
|
properties=SpreadsheetProperties(title=title),
|
|
@@ -112,3 +107,120 @@ def write_to_cell(
|
|
|
112
107
|
)
|
|
113
108
|
|
|
114
109
|
return parse_write_to_cell_response(sheet_properties)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@tool(
|
|
113
|
+
requires_auth=Google(
|
|
114
|
+
scopes=["https://www.googleapis.com/auth/drive.file"],
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
def update_cells(
|
|
118
|
+
context: ToolContext,
|
|
119
|
+
spreadsheet_id: Annotated[str, "The id of the spreadsheet to write to"],
|
|
120
|
+
data: Annotated[
|
|
121
|
+
str,
|
|
122
|
+
"The data to write. A JSON string (property names enclosed in double quotes) "
|
|
123
|
+
"representing a dictionary that maps row numbers to dictionaries that map "
|
|
124
|
+
"column letters to cell values. For example, data[23]['C'] is the value for cell C23. "
|
|
125
|
+
"This is the same format accepted by create_spreadsheet. "
|
|
126
|
+
"Type hint: dict[int, dict[str, int | float | str | bool]]",
|
|
127
|
+
],
|
|
128
|
+
sheet_position: Annotated[
|
|
129
|
+
int | None,
|
|
130
|
+
"The position/tab of the sheet in the spreadsheet to write to. "
|
|
131
|
+
"A value of 1 represents the first (leftmost/Sheet1) sheet. "
|
|
132
|
+
"Defaults to 1.",
|
|
133
|
+
] = 1,
|
|
134
|
+
sheet_id_or_name: Annotated[
|
|
135
|
+
str | None,
|
|
136
|
+
"The id or name of the sheet to write to. If provided, takes "
|
|
137
|
+
"precedence over sheet_position.",
|
|
138
|
+
] = None,
|
|
139
|
+
) -> Annotated[dict, "The status of the operation, including updated ranges and counts"]:
|
|
140
|
+
"""
|
|
141
|
+
Write values to a Google Sheet using a flexible data format.
|
|
142
|
+
|
|
143
|
+
sheet_id_or_name takes precedence over sheet_position. If a sheet is not mentioned,
|
|
144
|
+
then always assume the default sheet_position is sufficient.
|
|
145
|
+
"""
|
|
146
|
+
service = build_sheets_service(context.get_auth_token_or_empty())
|
|
147
|
+
|
|
148
|
+
sheet_data = validate_sheet_data_input(data)
|
|
149
|
+
sheet_name, sheet_id, sheet_url = get_sheet_metadata_from_identifier(
|
|
150
|
+
service, spreadsheet_id, sheet_position, sheet_id_or_name
|
|
151
|
+
)
|
|
152
|
+
converter = SheetDataInputToValueRangesConverter(sheet_name, sheet_data)
|
|
153
|
+
value_ranges = converter.convert()
|
|
154
|
+
|
|
155
|
+
response = batch_update(service, spreadsheet_id, value_ranges)
|
|
156
|
+
|
|
157
|
+
return {**response, "sheet_url": sheet_url, "sheet_id": sheet_id}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@tool(
|
|
161
|
+
requires_auth=Google(
|
|
162
|
+
scopes=["https://www.googleapis.com/auth/drive.file"],
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
def add_note_to_cell(
|
|
166
|
+
context: ToolContext,
|
|
167
|
+
spreadsheet_id: Annotated[str, "The id of the spreadsheet to add a comment to"],
|
|
168
|
+
column: Annotated[str, "The column string to add a note to. For example, 'A', 'F', or 'AZ'"],
|
|
169
|
+
row: Annotated[int, "The row number to add a note to"],
|
|
170
|
+
note_text: Annotated[str, "The text for the note to add"],
|
|
171
|
+
sheet_position: Annotated[
|
|
172
|
+
int | None,
|
|
173
|
+
"The position/tab of the sheet in the spreadsheet to write to. "
|
|
174
|
+
"A value of 1 represents the first (leftmost/Sheet1) sheet. "
|
|
175
|
+
"Defaults to 1.",
|
|
176
|
+
] = 1,
|
|
177
|
+
sheet_id_or_name: Annotated[
|
|
178
|
+
str | None,
|
|
179
|
+
"The id or name of the sheet to write to. If provided, takes "
|
|
180
|
+
"precedence over sheet_position.",
|
|
181
|
+
] = None,
|
|
182
|
+
) -> Annotated[dict, "The status of the operation"]:
|
|
183
|
+
"""
|
|
184
|
+
Add a note to a specific cell in a spreadsheet. A note is a small
|
|
185
|
+
piece of text attached to a cell (shown with a black triangle) that
|
|
186
|
+
appears when you hover over the cell.
|
|
187
|
+
|
|
188
|
+
sheet_id_or_name takes precedence over sheet_position. If a sheet is not mentioned,
|
|
189
|
+
then always assume the default sheet_position is sufficient.
|
|
190
|
+
"""
|
|
191
|
+
service = build_sheets_service(context.get_auth_token_or_empty())
|
|
192
|
+
|
|
193
|
+
sheet_name, sheet_id, sheet_url = get_sheet_metadata_from_identifier(
|
|
194
|
+
service, spreadsheet_id, sheet_position, sheet_id_or_name
|
|
195
|
+
)
|
|
196
|
+
column_index = col_to_index(column)
|
|
197
|
+
|
|
198
|
+
service.spreadsheets().batchUpdate(
|
|
199
|
+
spreadsheetId=spreadsheet_id,
|
|
200
|
+
body={
|
|
201
|
+
"requests": [
|
|
202
|
+
{
|
|
203
|
+
"repeatCell": {
|
|
204
|
+
"range": {
|
|
205
|
+
"sheetId": sheet_id,
|
|
206
|
+
"startRowIndex": row - 1,
|
|
207
|
+
"endRowIndex": row,
|
|
208
|
+
"startColumnIndex": column_index,
|
|
209
|
+
"endColumnIndex": column_index + 1,
|
|
210
|
+
},
|
|
211
|
+
"cell": {
|
|
212
|
+
"note": note_text,
|
|
213
|
+
},
|
|
214
|
+
"fields": "note",
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
]
|
|
218
|
+
},
|
|
219
|
+
).execute()
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
"status": "success",
|
|
223
|
+
"sheet_url": sheet_url,
|
|
224
|
+
"sheet_id": sheet_id,
|
|
225
|
+
"sheet_name": sheet_name,
|
|
226
|
+
}
|
arcade_google_sheets/utils.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
import string
|
|
2
3
|
from typing import Any
|
|
3
4
|
|
|
4
5
|
from arcade_tdk.errors import RetryableToolError, ToolExecutionError
|
|
@@ -9,7 +10,12 @@ from arcade_google_sheets.constants import (
|
|
|
9
10
|
DEFAULT_SHEET_COLUMN_COUNT,
|
|
10
11
|
DEFAULT_SHEET_ROW_COUNT,
|
|
11
12
|
)
|
|
12
|
-
from arcade_google_sheets.enums import
|
|
13
|
+
from arcade_google_sheets.enums import (
|
|
14
|
+
Corpora,
|
|
15
|
+
NumberFormatType,
|
|
16
|
+
OrderBy,
|
|
17
|
+
SheetIdentifierType,
|
|
18
|
+
)
|
|
13
19
|
from arcade_google_sheets.models import (
|
|
14
20
|
CellData,
|
|
15
21
|
CellExtendedValue,
|
|
@@ -21,7 +27,10 @@ from arcade_google_sheets.models import (
|
|
|
21
27
|
Sheet,
|
|
22
28
|
SheetDataInput,
|
|
23
29
|
SheetProperties,
|
|
30
|
+
Spreadsheet,
|
|
31
|
+
ValueRange,
|
|
24
32
|
)
|
|
33
|
+
from arcade_google_sheets.templates import sheet_url_template
|
|
25
34
|
from arcade_google_sheets.types import CellValue
|
|
26
35
|
|
|
27
36
|
logging.basicConfig(
|
|
@@ -32,6 +41,15 @@ logging.basicConfig(
|
|
|
32
41
|
logger = logging.getLogger(__name__)
|
|
33
42
|
|
|
34
43
|
|
|
44
|
+
def remove_none_values(params: dict) -> dict:
|
|
45
|
+
"""
|
|
46
|
+
Remove None values from a dictionary.
|
|
47
|
+
:param params: The dictionary to clean
|
|
48
|
+
:return: A new dictionary with None values removed
|
|
49
|
+
"""
|
|
50
|
+
return {k: v for k, v in params.items() if v is not None}
|
|
51
|
+
|
|
52
|
+
|
|
35
53
|
def build_sheets_service(auth_token: str | None) -> Resource: # type: ignore[no-any-unimported]
|
|
36
54
|
"""
|
|
37
55
|
Build a Sheets service object.
|
|
@@ -40,6 +58,14 @@ def build_sheets_service(auth_token: str | None) -> Resource: # type: ignore[no
|
|
|
40
58
|
return build("sheets", "v4", credentials=Credentials(auth_token))
|
|
41
59
|
|
|
42
60
|
|
|
61
|
+
def build_drive_service(auth_token: str | None) -> Resource: # type: ignore[no-any-unimported]
|
|
62
|
+
"""
|
|
63
|
+
Build a Drive service object.
|
|
64
|
+
"""
|
|
65
|
+
auth_token = auth_token or ""
|
|
66
|
+
return build("drive", "v3", credentials=Credentials(auth_token))
|
|
67
|
+
|
|
68
|
+
|
|
43
69
|
def col_to_index(col: str) -> int:
|
|
44
70
|
"""Convert a sheet's column string to a 0-indexed column index
|
|
45
71
|
|
|
@@ -459,6 +485,34 @@ def convert_api_grid_data_to_dict(grids: list[dict]) -> dict:
|
|
|
459
485
|
return dict(sorted(result.items()))
|
|
460
486
|
|
|
461
487
|
|
|
488
|
+
def validate_sheet_data_input(data: str | None) -> SheetDataInput:
|
|
489
|
+
"""
|
|
490
|
+
Validate and convert data to SheetDataInput, raising RetryableToolError on validation failure.
|
|
491
|
+
`data` is a JSON string representing a dictionary that maps row numbers to dictionaries that map
|
|
492
|
+
column letters to cell values.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
data: The data parameter to validate, a JSON string representing a dictionary that maps
|
|
496
|
+
row numbers to dictionaries that map column letters to cell values.
|
|
497
|
+
Type hint: dict[int, dict[str, int | float | str | bool]]
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
SheetDataInput: The validated sheet data input object
|
|
501
|
+
|
|
502
|
+
Raises:
|
|
503
|
+
RetryableToolError: If the data is invalid JSON or has an unexpected format
|
|
504
|
+
"""
|
|
505
|
+
try:
|
|
506
|
+
return SheetDataInput(data=data) # type: ignore[arg-type]
|
|
507
|
+
except Exception as e:
|
|
508
|
+
msg = "Invalid JSON or unexpected data format for parameter `data`"
|
|
509
|
+
raise RetryableToolError(
|
|
510
|
+
message=msg,
|
|
511
|
+
additional_prompt_content=f"{msg}: {e}",
|
|
512
|
+
retry_after_ms=100,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
|
|
462
516
|
def validate_write_to_cell_params( # type: ignore[no-any-unimported]
|
|
463
517
|
service: Resource,
|
|
464
518
|
spreadsheet_id: str,
|
|
@@ -546,3 +600,424 @@ def parse_write_to_cell_response(response: dict) -> dict:
|
|
|
546
600
|
"updatedCell": response["updatedData"]["range"].split("!")[1],
|
|
547
601
|
"value": response["updatedData"]["values"][0][0],
|
|
548
602
|
}
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def calculate_a1_sheet_range(
|
|
606
|
+
sheet_name: str,
|
|
607
|
+
sheet_row_count: int,
|
|
608
|
+
sheet_col_count: int,
|
|
609
|
+
start_row: int,
|
|
610
|
+
start_col: str,
|
|
611
|
+
max_rows: int,
|
|
612
|
+
max_cols: int,
|
|
613
|
+
) -> str | None:
|
|
614
|
+
"""Calculate a single range for a sheet based on start position and limits.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
sheet_name (str): The name of the sheet.
|
|
618
|
+
sheet_row_count (int): The number of rows in the sheet.
|
|
619
|
+
sheet_col_count (int): The number of columns in the sheet.
|
|
620
|
+
start_row (int): The row from which to start fetching data.
|
|
621
|
+
start_col (str): The column letter(s) from which to start fetching data.
|
|
622
|
+
max_rows (int): The maximum number of rows to fetch.
|
|
623
|
+
max_cols (int): The maximum number of columns to fetch.
|
|
624
|
+
|
|
625
|
+
Returns:
|
|
626
|
+
str | None: The A1 range for the sheet, or None if there is no data to fetch.
|
|
627
|
+
"""
|
|
628
|
+
start_col_index = col_to_index(start_col)
|
|
629
|
+
|
|
630
|
+
effective_max_rows = min(sheet_row_count, max_rows or sheet_row_count)
|
|
631
|
+
effective_max_cols = min(sheet_col_count, max_cols or sheet_col_count)
|
|
632
|
+
|
|
633
|
+
end_row = min(start_row + effective_max_rows - 1, sheet_row_count)
|
|
634
|
+
end_col_index = min(start_col_index + effective_max_cols - 1, sheet_col_count - 1)
|
|
635
|
+
|
|
636
|
+
# Only create a range if there's actually data to fetch
|
|
637
|
+
if start_row <= end_row and start_col_index <= end_col_index:
|
|
638
|
+
range_start = f"{index_to_col(start_col_index)}{start_row}"
|
|
639
|
+
range_end = f"{index_to_col(end_col_index)}{end_row}"
|
|
640
|
+
return f"'{sheet_name}'!{range_start}:{range_end}"
|
|
641
|
+
|
|
642
|
+
return None
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def get_sheet_by_identifier(
|
|
646
|
+
sheets: list[Sheet], sheet_identifier: str, sheet_identifier_type: SheetIdentifierType
|
|
647
|
+
) -> Sheet | None:
|
|
648
|
+
"""
|
|
649
|
+
Find a sheet by identifier (name, sheet ID, or 1-based position index).
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
sheets (list): List of Sheet objects from the spreadsheet.
|
|
653
|
+
sheet_identifier (str): The identifier of the sheet to get.
|
|
654
|
+
sheet_identifier_type (SheetIdentifierType): The type of the identifier.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
Sheet | None: The matching sheet, or None if not found.
|
|
658
|
+
"""
|
|
659
|
+
if sheet_identifier_type == SheetIdentifierType.POSITION:
|
|
660
|
+
index = int(sheet_identifier) - 1
|
|
661
|
+
if 0 <= index < len(sheets):
|
|
662
|
+
return sheets[index]
|
|
663
|
+
|
|
664
|
+
if sheet_identifier_type == SheetIdentifierType.ID_OR_NAME:
|
|
665
|
+
for sheet in sheets:
|
|
666
|
+
sheet_title = sheet.properties.title
|
|
667
|
+
sheet_id = sheet.properties.sheetId
|
|
668
|
+
if (
|
|
669
|
+
sheet_title.casefold() == sheet_identifier.casefold()
|
|
670
|
+
or str(sheet_id).casefold() == sheet_identifier.casefold()
|
|
671
|
+
):
|
|
672
|
+
return sheet
|
|
673
|
+
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def get_spreadsheet_metadata_helper(sheets_service: Resource, spreadsheet_id: str) -> Spreadsheet: # type: ignore[no-any-unimported]
|
|
678
|
+
"""Get the spreadsheet metadata to collect the sheet names and dimensions
|
|
679
|
+
|
|
680
|
+
Args:
|
|
681
|
+
sheets_service (Resource): The Google Sheets service.
|
|
682
|
+
spreadsheet_id (str): The ID of the spreadsheet provided to the tool.
|
|
683
|
+
|
|
684
|
+
Returns:
|
|
685
|
+
Spreadsheet: The spreadsheet with only the metadata.
|
|
686
|
+
"""
|
|
687
|
+
metadata_response = (
|
|
688
|
+
sheets_service.spreadsheets()
|
|
689
|
+
.get(
|
|
690
|
+
spreadsheetId=spreadsheet_id,
|
|
691
|
+
includeGridData=False,
|
|
692
|
+
fields="spreadsheetId,spreadsheetUrl,properties/title,sheets/properties",
|
|
693
|
+
)
|
|
694
|
+
.execute()
|
|
695
|
+
)
|
|
696
|
+
return Spreadsheet.model_validate(metadata_response)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def batch_update(service: Resource, spreadsheet_id: str, data: list[ValueRange]) -> dict: # type: ignore[no-any-unimported]
|
|
700
|
+
"""
|
|
701
|
+
Batch update a spreadsheet with a list of ValueRanges.
|
|
702
|
+
|
|
703
|
+
Args:
|
|
704
|
+
service (Resource): The Google Sheets service.
|
|
705
|
+
spreadsheet_id (str): The ID of the spreadsheet to update.
|
|
706
|
+
data (list[ValueRange]): The data to update the spreadsheet with.
|
|
707
|
+
|
|
708
|
+
Returns:
|
|
709
|
+
dict: The response from the batch update.
|
|
710
|
+
"""
|
|
711
|
+
body = {
|
|
712
|
+
"valueInputOption": "USER_ENTERED",
|
|
713
|
+
"data": [value_range.model_dump() for value_range in data],
|
|
714
|
+
}
|
|
715
|
+
response = (
|
|
716
|
+
service.spreadsheets()
|
|
717
|
+
.values()
|
|
718
|
+
.batchUpdate(spreadsheetId=spreadsheet_id, body=body)
|
|
719
|
+
.execute()
|
|
720
|
+
)
|
|
721
|
+
updated_ranges = [
|
|
722
|
+
value_response["updatedRange"] for value_response in response.get("responses", [])
|
|
723
|
+
]
|
|
724
|
+
return {
|
|
725
|
+
"spreadsheet_id": response["spreadsheetId"],
|
|
726
|
+
"total_updated_rows": response["totalUpdatedRows"],
|
|
727
|
+
"total_updated_columns": response["totalUpdatedColumns"],
|
|
728
|
+
"total_updated_cells": response["totalUpdatedCells"],
|
|
729
|
+
"updated_ranges": updated_ranges,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
|
|
733
|
+
def get_spreadsheet_with_pagination( # type: ignore[no-any-unimported]
|
|
734
|
+
service: Resource,
|
|
735
|
+
spreadsheet_id: str,
|
|
736
|
+
sheet_identifier: str,
|
|
737
|
+
sheet_identifier_type: SheetIdentifierType,
|
|
738
|
+
start_row: int,
|
|
739
|
+
start_col: str,
|
|
740
|
+
max_rows: int,
|
|
741
|
+
max_cols: int,
|
|
742
|
+
) -> dict:
|
|
743
|
+
"""
|
|
744
|
+
Get spreadsheet data with pagination support for large spreadsheets.
|
|
745
|
+
|
|
746
|
+
Args:
|
|
747
|
+
service (Resource): The Google Sheets service.
|
|
748
|
+
spreadsheet_id (str): The ID of the spreadsheet provided to the tool.
|
|
749
|
+
sheet_position (int | None): The position/tab of the sheet to get.
|
|
750
|
+
sheet_id_or_name (str | None): The id or name of the sheet to get.
|
|
751
|
+
start_row (int): The row from which to start fetching data.
|
|
752
|
+
start_col (str): The column letter(s) from which to start fetching data.
|
|
753
|
+
max_rows (int): The maximum number of rows to fetch.
|
|
754
|
+
max_cols (int): The maximum number of columns to fetch.
|
|
755
|
+
|
|
756
|
+
Returns:
|
|
757
|
+
dict: The spreadsheet data for the specified sheet in the spreadsheet.
|
|
758
|
+
|
|
759
|
+
"""
|
|
760
|
+
|
|
761
|
+
# First, only get the spreadsheet metadata to collect the sheet names and dimensions
|
|
762
|
+
spreadsheet_with_only_metadata = get_spreadsheet_metadata_helper(service, spreadsheet_id)
|
|
763
|
+
|
|
764
|
+
target_sheet = get_sheet_by_identifier(
|
|
765
|
+
spreadsheet_with_only_metadata.sheets, sheet_identifier, sheet_identifier_type
|
|
766
|
+
)
|
|
767
|
+
if not target_sheet:
|
|
768
|
+
raise ToolExecutionError(
|
|
769
|
+
message=f"Sheet with identifier '{sheet_identifier}' not found",
|
|
770
|
+
developer_message=(
|
|
771
|
+
"Sheet(s) in the spreadsheet: "
|
|
772
|
+
+ ", ".join([
|
|
773
|
+
sheet.model_dump_json() for sheet in spreadsheet_with_only_metadata.sheets
|
|
774
|
+
])
|
|
775
|
+
),
|
|
776
|
+
)
|
|
777
|
+
|
|
778
|
+
a1_ranges = []
|
|
779
|
+
sheet_name = target_sheet.properties.title
|
|
780
|
+
grid_props = target_sheet.properties.gridProperties
|
|
781
|
+
if grid_props:
|
|
782
|
+
sheet_row_count = grid_props.rowCount
|
|
783
|
+
sheet_col_count = grid_props.columnCount
|
|
784
|
+
|
|
785
|
+
curr_range = calculate_a1_sheet_range(
|
|
786
|
+
sheet_name,
|
|
787
|
+
sheet_row_count,
|
|
788
|
+
sheet_col_count,
|
|
789
|
+
start_row,
|
|
790
|
+
start_col,
|
|
791
|
+
max_rows,
|
|
792
|
+
max_cols,
|
|
793
|
+
)
|
|
794
|
+
if curr_range:
|
|
795
|
+
a1_ranges.append(curr_range)
|
|
796
|
+
|
|
797
|
+
# Next, get the data for the ranges
|
|
798
|
+
if a1_ranges:
|
|
799
|
+
response = (
|
|
800
|
+
service.spreadsheets()
|
|
801
|
+
.get(
|
|
802
|
+
spreadsheetId=spreadsheet_id,
|
|
803
|
+
includeGridData=True,
|
|
804
|
+
ranges=a1_ranges,
|
|
805
|
+
fields="spreadsheetId,spreadsheetUrl,properties/title,sheets/properties,sheets/data/rowData/values/userEnteredValue,sheets/data/rowData/values/formattedValue,sheets/data/rowData/values/effectiveValue",
|
|
806
|
+
)
|
|
807
|
+
.execute()
|
|
808
|
+
)
|
|
809
|
+
else:
|
|
810
|
+
response = spreadsheet_with_only_metadata.model_dump()
|
|
811
|
+
|
|
812
|
+
return parse_get_spreadsheet_response(response)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def process_get_spreadsheet_params(
|
|
816
|
+
sheet_position: int | None,
|
|
817
|
+
sheet_id_or_name: str | None,
|
|
818
|
+
start_row: int,
|
|
819
|
+
start_col: str,
|
|
820
|
+
max_rows: int,
|
|
821
|
+
max_cols: int,
|
|
822
|
+
) -> tuple[str, SheetIdentifierType, int, str, int, int]:
|
|
823
|
+
"""Process and validate the input parameters for the get_spreadsheet tool.
|
|
824
|
+
|
|
825
|
+
Args:
|
|
826
|
+
sheet_position (int | None): The position/tab of the sheet to get.
|
|
827
|
+
sheet_id_or_name (str | None): The id or name of the sheet to get.
|
|
828
|
+
start_row (int): Processed to be within the range [1, 1000]
|
|
829
|
+
start_col (str): Processed to be alphabetic column representation. e.g., A, Z, QED
|
|
830
|
+
max_rows (int): Processed to be within the range [1, 1000]
|
|
831
|
+
max_cols (int): Processed to be within the range [1, 26]
|
|
832
|
+
|
|
833
|
+
Returns:
|
|
834
|
+
tuple[str, str, int, str, int, int]: The processed parameters.
|
|
835
|
+
|
|
836
|
+
Raises:
|
|
837
|
+
ToolExecutionError:
|
|
838
|
+
If the start_col is not one of alphabetic or numeric
|
|
839
|
+
"""
|
|
840
|
+
if sheet_id_or_name:
|
|
841
|
+
sheet_identifier = sheet_id_or_name
|
|
842
|
+
sheet_identifier_type = SheetIdentifierType.ID_OR_NAME
|
|
843
|
+
elif sheet_position:
|
|
844
|
+
sheet_identifier = str(sheet_position)
|
|
845
|
+
sheet_identifier_type = SheetIdentifierType.POSITION
|
|
846
|
+
else:
|
|
847
|
+
raise RetryableToolError("Either sheet_position or sheet_id_or_name must be provided")
|
|
848
|
+
|
|
849
|
+
processed_start_row = max(1, start_row)
|
|
850
|
+
processed_max_rows = max(1, min(max_rows, 1000))
|
|
851
|
+
processed_max_cols = max(1, min(max_cols, 26))
|
|
852
|
+
if not all(c in string.ascii_letters for c in start_col):
|
|
853
|
+
if not start_col.isdigit():
|
|
854
|
+
raise ToolExecutionError("Input 'start_col' must be alphabetic (A-Z) or numeric")
|
|
855
|
+
processed_start_col = index_to_col(int(start_col) - 1)
|
|
856
|
+
else:
|
|
857
|
+
processed_start_col = start_col.upper()
|
|
858
|
+
|
|
859
|
+
return (
|
|
860
|
+
sheet_identifier,
|
|
861
|
+
sheet_identifier_type,
|
|
862
|
+
processed_start_row,
|
|
863
|
+
processed_start_col,
|
|
864
|
+
processed_max_rows,
|
|
865
|
+
processed_max_cols,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
def get_sheet_metadata_from_identifier( # type: ignore[no-any-unimported]
|
|
870
|
+
service: Resource,
|
|
871
|
+
spreadsheet_id: str,
|
|
872
|
+
sheet_position: int | None,
|
|
873
|
+
sheet_id_or_name: str | None,
|
|
874
|
+
) -> tuple[str, int, str]:
|
|
875
|
+
"""Get the actual sheet name from position, id, or name identifier.
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
service (Resource): The Google Sheets service.
|
|
879
|
+
spreadsheet_id (str): The ID of the spreadsheet.
|
|
880
|
+
sheet_position (int | None): The position/tab of the sheet (1-indexed).
|
|
881
|
+
sheet_id_or_name (str | None): The id or name of the sheet.
|
|
882
|
+
|
|
883
|
+
Returns:
|
|
884
|
+
tuple[str, str, str]: The sheet's title, id, and url.
|
|
885
|
+
|
|
886
|
+
Raises:
|
|
887
|
+
ToolExecutionError: If the sheet is not found.
|
|
888
|
+
"""
|
|
889
|
+
# Determine the sheet identifier and type
|
|
890
|
+
if sheet_id_or_name:
|
|
891
|
+
sheet_identifier = sheet_id_or_name
|
|
892
|
+
sheet_identifier_type = SheetIdentifierType.ID_OR_NAME
|
|
893
|
+
elif sheet_position:
|
|
894
|
+
sheet_identifier = str(sheet_position)
|
|
895
|
+
sheet_identifier_type = SheetIdentifierType.POSITION
|
|
896
|
+
else:
|
|
897
|
+
# Default to first sheet
|
|
898
|
+
sheet_identifier = "1"
|
|
899
|
+
sheet_identifier_type = SheetIdentifierType.POSITION
|
|
900
|
+
|
|
901
|
+
spreadsheet = get_spreadsheet_metadata_helper(service, spreadsheet_id)
|
|
902
|
+
|
|
903
|
+
target_sheet = get_sheet_by_identifier(
|
|
904
|
+
spreadsheet.sheets, sheet_identifier, sheet_identifier_type
|
|
905
|
+
)
|
|
906
|
+
|
|
907
|
+
if not target_sheet:
|
|
908
|
+
raise ToolExecutionError(
|
|
909
|
+
message=f"Sheet with {sheet_identifier_type.value} '{sheet_identifier}' not found",
|
|
910
|
+
developer_message=(
|
|
911
|
+
"Sheet(s) in the spreadsheet: "
|
|
912
|
+
+ ", ".join([sheet.properties.title for sheet in spreadsheet.sheets])
|
|
913
|
+
),
|
|
914
|
+
)
|
|
915
|
+
|
|
916
|
+
sheet_url = sheet_url_template.format(
|
|
917
|
+
spreadsheet_id=spreadsheet_id,
|
|
918
|
+
sheet_id=target_sheet.properties.sheetId,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
return target_sheet.properties.title, target_sheet.properties.sheetId, sheet_url
|
|
922
|
+
|
|
923
|
+
|
|
924
|
+
def raise_for_large_payload(data: dict) -> None:
|
|
925
|
+
"""Enforce a 10MB limit on the data size.
|
|
926
|
+
|
|
927
|
+
Args:
|
|
928
|
+
data (dict): The data to enforce the size limit on.
|
|
929
|
+
|
|
930
|
+
Raises:
|
|
931
|
+
ToolExecutionError:
|
|
932
|
+
If the data size exceeds 10MB
|
|
933
|
+
"""
|
|
934
|
+
num_bytes = len(str(data).encode("utf-8"))
|
|
935
|
+
|
|
936
|
+
if num_bytes >= (10 * 1024 * 1024):
|
|
937
|
+
raise ToolExecutionError(
|
|
938
|
+
message="Spreadsheet size exceeds 10MB limit. "
|
|
939
|
+
"Please reduce the number of rows and columns you are requesting and try again.",
|
|
940
|
+
developer_message=f"Data size: {num_bytes / 1024 / 1024:.4f}MB",
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
# ------------------------------
|
|
945
|
+
# Search Utils
|
|
946
|
+
# ------------------------------
|
|
947
|
+
def build_files_list_query(
|
|
948
|
+
mime_type: str,
|
|
949
|
+
document_contains: list[str] | None = None,
|
|
950
|
+
document_not_contains: list[str] | None = None,
|
|
951
|
+
) -> str:
|
|
952
|
+
query = [f"(mimeType = '{mime_type}' and trashed = false)"]
|
|
953
|
+
|
|
954
|
+
if isinstance(document_contains, str):
|
|
955
|
+
document_contains = [document_contains]
|
|
956
|
+
|
|
957
|
+
if isinstance(document_not_contains, str):
|
|
958
|
+
document_not_contains = [document_not_contains]
|
|
959
|
+
|
|
960
|
+
if document_contains:
|
|
961
|
+
for keyword in document_contains:
|
|
962
|
+
name_contains = keyword.replace("'", "\\'")
|
|
963
|
+
full_text_contains = keyword.replace("'", "\\'")
|
|
964
|
+
keyword_query = (
|
|
965
|
+
f"(name contains '{name_contains}' or fullText contains '{full_text_contains}')"
|
|
966
|
+
)
|
|
967
|
+
query.append(keyword_query)
|
|
968
|
+
|
|
969
|
+
if document_not_contains:
|
|
970
|
+
for keyword in document_not_contains:
|
|
971
|
+
name_not_contains = keyword.replace("'", "\\'")
|
|
972
|
+
full_text_not_contains = keyword.replace("'", "\\'")
|
|
973
|
+
keyword_query = (
|
|
974
|
+
f"(name not contains '{name_not_contains}' and "
|
|
975
|
+
f"fullText not contains '{full_text_not_contains}')"
|
|
976
|
+
)
|
|
977
|
+
query.append(keyword_query)
|
|
978
|
+
|
|
979
|
+
return " and ".join(query)
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def build_files_list_params(
|
|
983
|
+
mime_type: str,
|
|
984
|
+
page_size: int,
|
|
985
|
+
order_by: list[OrderBy],
|
|
986
|
+
pagination_token: str | None,
|
|
987
|
+
include_shared_drives: bool,
|
|
988
|
+
search_only_in_shared_drive_id: str | None,
|
|
989
|
+
include_organization_domain_spreadsheets: bool,
|
|
990
|
+
spreadsheet_contains: list[str] | None = None,
|
|
991
|
+
spreadsheet_not_contains: list[str] | None = None,
|
|
992
|
+
) -> dict[str, Any]:
|
|
993
|
+
query = build_files_list_query(
|
|
994
|
+
mime_type=mime_type,
|
|
995
|
+
document_contains=spreadsheet_contains,
|
|
996
|
+
document_not_contains=spreadsheet_not_contains,
|
|
997
|
+
)
|
|
998
|
+
|
|
999
|
+
params = {
|
|
1000
|
+
"q": query,
|
|
1001
|
+
"pageSize": page_size,
|
|
1002
|
+
"orderBy": ",".join([item.value for item in order_by]),
|
|
1003
|
+
"pageToken": pagination_token,
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
if (
|
|
1007
|
+
include_shared_drives
|
|
1008
|
+
or search_only_in_shared_drive_id
|
|
1009
|
+
or include_organization_domain_spreadsheets
|
|
1010
|
+
):
|
|
1011
|
+
params["includeItemsFromAllDrives"] = "true"
|
|
1012
|
+
params["supportsAllDrives"] = "true"
|
|
1013
|
+
|
|
1014
|
+
if search_only_in_shared_drive_id:
|
|
1015
|
+
params["driveId"] = search_only_in_shared_drive_id
|
|
1016
|
+
params["corpora"] = Corpora.DRIVE.value
|
|
1017
|
+
|
|
1018
|
+
if include_organization_domain_spreadsheets:
|
|
1019
|
+
params["corpora"] = Corpora.DOMAIN.value
|
|
1020
|
+
|
|
1021
|
+
params = remove_none_values(params)
|
|
1022
|
+
|
|
1023
|
+
return params
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arcade_google_sheets
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Arcade.dev LLM tools for Google Sheets
|
|
5
5
|
Author-email: Arcade <dev@arcade.dev>
|
|
6
|
+
License: Proprietary - Arcade Software License Agreement v1.0
|
|
6
7
|
License-File: LICENSE
|
|
7
8
|
Requires-Python: >=3.10
|
|
8
9
|
Requires-Dist: arcade-tdk<3.0.0,>=2.0.0
|
|
@@ -12,7 +13,7 @@ Requires-Dist: google-auth-httplib2<1.0.0,>=0.2.0
|
|
|
12
13
|
Requires-Dist: google-auth<3.0.0,>=2.32.0
|
|
13
14
|
Requires-Dist: googleapis-common-protos<2.0.0,>=1.63.2
|
|
14
15
|
Provides-Extra: dev
|
|
15
|
-
Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.
|
|
16
|
+
Requires-Dist: arcade-ai[evals]<3.0.0,>=2.0.0; extra == 'dev'
|
|
16
17
|
Requires-Dist: arcade-serve<3.0.0,>=2.0.0; extra == 'dev'
|
|
17
18
|
Requires-Dist: mypy<1.6.0,>=1.5.1; extra == 'dev'
|
|
18
19
|
Requires-Dist: pre-commit<3.5.0,>=3.4.0; extra == 'dev'
|