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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from arcade_google_sheets.enums import Dimension
|
|
2
|
+
from arcade_google_sheets.models import CellValue, SheetDataInput, ValueRange
|
|
3
|
+
from arcade_google_sheets.utils import (
|
|
4
|
+
col_to_index,
|
|
5
|
+
group_contiguous_rows,
|
|
6
|
+
index_to_col,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SheetDataInputToValueRangesConverter:
|
|
11
|
+
def __init__(self, sheet_name: str, sheet_data: SheetDataInput):
|
|
12
|
+
self.sheet_name = sheet_name
|
|
13
|
+
self.sheet_data = sheet_data
|
|
14
|
+
|
|
15
|
+
def convert(self) -> list[ValueRange]:
|
|
16
|
+
"""
|
|
17
|
+
Convert a SheetDataInput to a list of ValueRanges that are row-oriented.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
sheet_name (str): The name of the sheet to which the data belongs.
|
|
21
|
+
sheet_data (SheetDataInput): The data to convert into ranges.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
list[ValueRange]: The converted ValueRanges.
|
|
25
|
+
"""
|
|
26
|
+
if not self.sheet_data.data:
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
row_ranges = self._build_row_oriented_ranges()
|
|
30
|
+
|
|
31
|
+
return row_ranges
|
|
32
|
+
|
|
33
|
+
def _to_float_if_int(self, value: CellValue) -> bool | str | float:
|
|
34
|
+
"""
|
|
35
|
+
The spreadsheets.values.batchUpdate API does not support int values.
|
|
36
|
+
So we convert ints to floats.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
value (Any): The value to possibly convert.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
bool | str | float: The converted value.
|
|
43
|
+
"""
|
|
44
|
+
if isinstance(value, bool):
|
|
45
|
+
return value
|
|
46
|
+
if isinstance(value, int):
|
|
47
|
+
return float(value)
|
|
48
|
+
return value
|
|
49
|
+
|
|
50
|
+
def _get_cell_value(self, row_num: int, col_idx: int) -> bool | str | float:
|
|
51
|
+
"""
|
|
52
|
+
Safely fetch a cell value.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
row_num (int): The row number of the cell.
|
|
56
|
+
col_idx (int): The column index of the cell.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
bool | str | float: The value of the cell.
|
|
60
|
+
"""
|
|
61
|
+
col_letter = index_to_col(col_idx)
|
|
62
|
+
return self._to_float_if_int(self.sheet_data.data[row_num][col_letter])
|
|
63
|
+
|
|
64
|
+
def _build_row_oriented_ranges(self) -> list[ValueRange]:
|
|
65
|
+
"""
|
|
66
|
+
Build row-oriented ValueRanges for the object's sheet data.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
list[ValueRange]: The row-oriented ValueRanges.
|
|
70
|
+
"""
|
|
71
|
+
# Map (start_col_idx, end_col_idx) -> { row_num: [values across columns] }
|
|
72
|
+
segment_to_rows_values: dict[tuple[int, int], dict[int, list[bool | str | float]]] = {}
|
|
73
|
+
|
|
74
|
+
for row_num in sorted(self.sheet_data.data):
|
|
75
|
+
cols_dict = self.sheet_data.data[row_num]
|
|
76
|
+
col_indices = sorted(col_to_index(col) for col in cols_dict)
|
|
77
|
+
if not col_indices:
|
|
78
|
+
continue
|
|
79
|
+
contiguous_groups = group_contiguous_rows(col_indices)
|
|
80
|
+
for group in contiguous_groups:
|
|
81
|
+
start_idx = group[0]
|
|
82
|
+
end_idx = group[-1]
|
|
83
|
+
row_values = [self._get_cell_value(row_num, ci) for ci in group]
|
|
84
|
+
key = (start_idx, end_idx)
|
|
85
|
+
if key not in segment_to_rows_values:
|
|
86
|
+
segment_to_rows_values[key] = {}
|
|
87
|
+
segment_to_rows_values[key][row_num] = row_values
|
|
88
|
+
|
|
89
|
+
row_oriented_ranges: list[ValueRange] = []
|
|
90
|
+
for (start_idx, end_idx), rows_map in segment_to_rows_values.items():
|
|
91
|
+
sorted_rows = sorted(rows_map.keys())
|
|
92
|
+
row_groups = group_contiguous_rows(sorted_rows)
|
|
93
|
+
for rg in row_groups:
|
|
94
|
+
start_row = rg[0]
|
|
95
|
+
end_row = rg[-1]
|
|
96
|
+
start_col = index_to_col(start_idx)
|
|
97
|
+
end_col = index_to_col(end_idx)
|
|
98
|
+
a1_range = f"'{self.sheet_name}'!{start_col}{start_row}:{end_col}{end_row}"
|
|
99
|
+
values = [rows_map[r] for r in rg]
|
|
100
|
+
row_oriented_ranges.append(
|
|
101
|
+
ValueRange(
|
|
102
|
+
range=a1_range,
|
|
103
|
+
majorDimension=Dimension.ROWS,
|
|
104
|
+
values=values,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return row_oriented_ranges
|
arcade_google_sheets/enums.py
CHANGED
|
@@ -23,3 +23,127 @@ class NumberFormatType(str, Enum):
|
|
|
23
23
|
NUMBER = "NUMBER"
|
|
24
24
|
PERCENT = "PERCENT"
|
|
25
25
|
CURRENCY = "CURRENCY"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SheetIdentifierType(str, Enum):
|
|
29
|
+
POSITION = "position"
|
|
30
|
+
ID_OR_NAME = "id_or_name"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Dimension(str, Enum):
|
|
34
|
+
ROWS = "ROWS" # Operates on the rows of a sheet.
|
|
35
|
+
COLUMNS = "COLUMNS" # Operates on the columns of a sheet.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ------------------------------------------------------------
|
|
39
|
+
# Drive API enums
|
|
40
|
+
# ------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OrderBy(str, Enum):
|
|
44
|
+
"""
|
|
45
|
+
Sort keys for ordering files in Google Drive.
|
|
46
|
+
Each key has both ascending and descending options.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
CREATED_TIME = (
|
|
50
|
+
# When the file was created (ascending)
|
|
51
|
+
"createdTime"
|
|
52
|
+
)
|
|
53
|
+
CREATED_TIME_DESC = (
|
|
54
|
+
# When the file was created (descending)
|
|
55
|
+
"createdTime desc"
|
|
56
|
+
)
|
|
57
|
+
FOLDER = (
|
|
58
|
+
# The folder ID, sorted using alphabetical ordering (ascending)
|
|
59
|
+
"folder"
|
|
60
|
+
)
|
|
61
|
+
FOLDER_DESC = (
|
|
62
|
+
# The folder ID, sorted using alphabetical ordering (descending)
|
|
63
|
+
"folder desc"
|
|
64
|
+
)
|
|
65
|
+
MODIFIED_BY_ME_TIME = (
|
|
66
|
+
# The last time the file was modified by the user (ascending)
|
|
67
|
+
"modifiedByMeTime"
|
|
68
|
+
)
|
|
69
|
+
MODIFIED_BY_ME_TIME_DESC = (
|
|
70
|
+
# The last time the file was modified by the user (descending)
|
|
71
|
+
"modifiedByMeTime desc"
|
|
72
|
+
)
|
|
73
|
+
MODIFIED_TIME = (
|
|
74
|
+
# The last time the file was modified by anyone (ascending)
|
|
75
|
+
"modifiedTime"
|
|
76
|
+
)
|
|
77
|
+
MODIFIED_TIME_DESC = (
|
|
78
|
+
# The last time the file was modified by anyone (descending)
|
|
79
|
+
"modifiedTime desc"
|
|
80
|
+
)
|
|
81
|
+
NAME = (
|
|
82
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (ascending)
|
|
83
|
+
"name"
|
|
84
|
+
)
|
|
85
|
+
NAME_DESC = (
|
|
86
|
+
# The name of the file, sorted using alphabetical ordering (e.g., 1, 12, 2, 22) (descending)
|
|
87
|
+
"name desc"
|
|
88
|
+
)
|
|
89
|
+
NAME_NATURAL = (
|
|
90
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (ascending)
|
|
91
|
+
"name_natural"
|
|
92
|
+
)
|
|
93
|
+
NAME_NATURAL_DESC = (
|
|
94
|
+
# The name of the file, sorted using natural sort ordering (e.g., 1, 2, 12, 22) (descending)
|
|
95
|
+
"name_natural desc"
|
|
96
|
+
)
|
|
97
|
+
QUOTA_BYTES_USED = (
|
|
98
|
+
# The number of storage quota bytes used by the file (ascending)
|
|
99
|
+
"quotaBytesUsed"
|
|
100
|
+
)
|
|
101
|
+
QUOTA_BYTES_USED_DESC = (
|
|
102
|
+
# The number of storage quota bytes used by the file (descending)
|
|
103
|
+
"quotaBytesUsed desc"
|
|
104
|
+
)
|
|
105
|
+
RECENCY = (
|
|
106
|
+
# The most recent timestamp from the file's date-time fields (ascending)
|
|
107
|
+
"recency"
|
|
108
|
+
)
|
|
109
|
+
RECENCY_DESC = (
|
|
110
|
+
# The most recent timestamp from the file's date-time fields (descending)
|
|
111
|
+
"recency desc"
|
|
112
|
+
)
|
|
113
|
+
SHARED_WITH_ME_TIME = (
|
|
114
|
+
# When the file was shared with the user, if applicable (ascending)
|
|
115
|
+
"sharedWithMeTime"
|
|
116
|
+
)
|
|
117
|
+
SHARED_WITH_ME_TIME_DESC = (
|
|
118
|
+
# When the file was shared with the user, if applicable (descending)
|
|
119
|
+
"sharedWithMeTime desc"
|
|
120
|
+
)
|
|
121
|
+
STARRED = (
|
|
122
|
+
# Whether the user has starred the file (ascending)
|
|
123
|
+
"starred"
|
|
124
|
+
)
|
|
125
|
+
STARRED_DESC = (
|
|
126
|
+
# Whether the user has starred the file (descending)
|
|
127
|
+
"starred desc"
|
|
128
|
+
)
|
|
129
|
+
VIEWED_BY_ME_TIME = (
|
|
130
|
+
# The last time the file was viewed by the user (ascending)
|
|
131
|
+
"viewedByMeTime"
|
|
132
|
+
)
|
|
133
|
+
VIEWED_BY_ME_TIME_DESC = (
|
|
134
|
+
# The last time the file was viewed by the user (descending)
|
|
135
|
+
"viewedByMeTime desc"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Corpora(str, Enum):
|
|
140
|
+
"""
|
|
141
|
+
Bodies of items (spreadsheets) to which the query applies.
|
|
142
|
+
Prefer 'user' or 'drive' to 'allDrives' for efficiency.
|
|
143
|
+
By default, corpora is set to 'user'.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
USER = "user"
|
|
147
|
+
DOMAIN = "domain"
|
|
148
|
+
DRIVE = "drive"
|
|
149
|
+
ALL_DRIVES = "allDrives"
|
arcade_google_sheets/models.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel, field_validator, model_validator
|
|
5
5
|
|
|
6
|
-
from arcade_google_sheets.enums import CellErrorType, NumberFormatType
|
|
6
|
+
from arcade_google_sheets.enums import CellErrorType, Dimension, NumberFormatType
|
|
7
7
|
from arcade_google_sheets.types import CellValue
|
|
8
8
|
|
|
9
9
|
|
|
@@ -137,6 +137,41 @@ class Spreadsheet(BaseModel):
|
|
|
137
137
|
|
|
138
138
|
properties: SpreadsheetProperties
|
|
139
139
|
sheets: list[Sheet]
|
|
140
|
+
spreadsheetId: str | None = None
|
|
141
|
+
spreadsheetUrl: str | None = None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class ValueRange(BaseModel):
|
|
145
|
+
"""A range of cells in a spreadsheet
|
|
146
|
+
|
|
147
|
+
An implementation of https://developers.google.com/workspace/sheets/api/reference/rest/v4/spreadsheets.values#ValueRange
|
|
148
|
+
|
|
149
|
+
Example 1:
|
|
150
|
+
{
|
|
151
|
+
"range": "Sheet1!A1:B2",
|
|
152
|
+
"majorDimension": "ROWS",
|
|
153
|
+
"values": [
|
|
154
|
+
["1", "2"],
|
|
155
|
+
["3", "4"]
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
Example 2:
|
|
159
|
+
{
|
|
160
|
+
"range": "Sheet1!A1:A4",
|
|
161
|
+
"majorDimension": "COLUMNS",
|
|
162
|
+
"values": [
|
|
163
|
+
["Item", "Wheel", "Door", "Engine"]
|
|
164
|
+
]
|
|
165
|
+
}
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
range: str # A1 notation
|
|
169
|
+
majorDimension: Dimension
|
|
170
|
+
# values is a 2D array. The outer array represents all the data and each inner
|
|
171
|
+
# array represents a major dimension. Each item in the inner array corresponds
|
|
172
|
+
# with one cell.
|
|
173
|
+
# Note: Google API docs don't mention support for int, so CellValue is not used
|
|
174
|
+
values: list[list[bool | str | float]]
|
|
140
175
|
|
|
141
176
|
|
|
142
177
|
class SheetDataInput(BaseModel):
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
optional_file_picker_instructions_template = (
|
|
2
|
+
"Ensure the user knows that they have the option to select and grant access permissions to "
|
|
3
|
+
"additional files and folders via the Google Drive File Picker. "
|
|
4
|
+
"The user can pick additional files and folders via the following link: {url}"
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
spreadsheet_url_template = "https://docs.google.com/spreadsheets/d/{spreadsheet_id}/edit"
|
|
8
|
+
sheet_url_template = "https://docs.google.com/spreadsheets/d/{spreadsheet_id}/edit#gid={sheet_id}"
|
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
from arcade_google_sheets.tools.read import get_spreadsheet
|
|
2
|
-
from arcade_google_sheets.tools.
|
|
1
|
+
from arcade_google_sheets.tools.read import get_spreadsheet, get_spreadsheet_metadata
|
|
2
|
+
from arcade_google_sheets.tools.search import search_spreadsheets
|
|
3
|
+
from arcade_google_sheets.tools.write import (
|
|
4
|
+
add_note_to_cell,
|
|
5
|
+
create_spreadsheet,
|
|
6
|
+
update_cells,
|
|
7
|
+
write_to_cell,
|
|
8
|
+
)
|
|
3
9
|
|
|
4
|
-
__all__ = [
|
|
10
|
+
__all__ = [
|
|
11
|
+
"create_spreadsheet",
|
|
12
|
+
"get_spreadsheet",
|
|
13
|
+
"get_spreadsheet_metadata",
|
|
14
|
+
"search_spreadsheets",
|
|
15
|
+
"update_cells",
|
|
16
|
+
"add_note_to_cell",
|
|
17
|
+
"write_to_cell",
|
|
18
|
+
]
|
|
@@ -4,9 +4,13 @@ from arcade_tdk import ToolContext, ToolMetadataKey, tool
|
|
|
4
4
|
from arcade_tdk.auth import Google
|
|
5
5
|
|
|
6
6
|
from arcade_google_sheets.decorators import with_filepicker_fallback
|
|
7
|
+
from arcade_google_sheets.templates import sheet_url_template
|
|
7
8
|
from arcade_google_sheets.utils import (
|
|
8
9
|
build_sheets_service,
|
|
9
|
-
|
|
10
|
+
get_spreadsheet_metadata_helper,
|
|
11
|
+
get_spreadsheet_with_pagination,
|
|
12
|
+
process_get_spreadsheet_params,
|
|
13
|
+
raise_for_large_payload,
|
|
10
14
|
)
|
|
11
15
|
|
|
12
16
|
|
|
@@ -20,23 +24,100 @@ from arcade_google_sheets.utils import (
|
|
|
20
24
|
async def get_spreadsheet(
|
|
21
25
|
context: ToolContext,
|
|
22
26
|
spreadsheet_id: Annotated[str, "The id of the spreadsheet to get"],
|
|
27
|
+
sheet_position: Annotated[
|
|
28
|
+
int | None,
|
|
29
|
+
"The position/tab of the sheet in the spreadsheet to get. "
|
|
30
|
+
"A value of 1 represents the first (leftmost/Sheet1) sheet . "
|
|
31
|
+
"Defaults to 1.",
|
|
32
|
+
] = 1,
|
|
33
|
+
sheet_id_or_name: Annotated[
|
|
34
|
+
str | None,
|
|
35
|
+
"The id or name of the sheet to get. "
|
|
36
|
+
"Defaults to None, which means sheet_position will be used instead.",
|
|
37
|
+
] = None,
|
|
38
|
+
start_row: Annotated[int, "Starting row number (1-indexed, defaults to 1)"] = 1,
|
|
39
|
+
start_col: Annotated[
|
|
40
|
+
str, "Starting column letter(s) or 1-based column number (defaults to 'A')"
|
|
41
|
+
] = "A",
|
|
42
|
+
max_rows: Annotated[
|
|
43
|
+
int,
|
|
44
|
+
"Maximum number of rows to fetch for each sheet in the spreadsheet. "
|
|
45
|
+
"Must be between 1 and 1000. Defaults to 1000.",
|
|
46
|
+
] = 1000,
|
|
47
|
+
max_cols: Annotated[
|
|
48
|
+
int,
|
|
49
|
+
"Maximum number of columns to fetch for each sheet in the spreadsheet. "
|
|
50
|
+
"Must be between 1 and 100. Defaults to 100.",
|
|
51
|
+
] = 100,
|
|
23
52
|
) -> Annotated[
|
|
24
53
|
dict,
|
|
25
|
-
"The spreadsheet properties and data for
|
|
54
|
+
"The spreadsheet properties and data for the specified sheet in the spreadsheet",
|
|
26
55
|
]:
|
|
56
|
+
"""Gets the specified range of cells from a single sheet in the spreadsheet.
|
|
57
|
+
|
|
58
|
+
sheet_id_or_name takes precedence over sheet_position. If a sheet is not mentioned,
|
|
59
|
+
then always assume the default sheet_position is sufficient.
|
|
27
60
|
"""
|
|
28
|
-
|
|
29
|
-
|
|
61
|
+
sheet_identifier, sheet_identifier_type, start_row, start_col, max_rows, max_cols = (
|
|
62
|
+
process_get_spreadsheet_params(
|
|
63
|
+
sheet_position,
|
|
64
|
+
sheet_id_or_name,
|
|
65
|
+
start_row,
|
|
66
|
+
start_col,
|
|
67
|
+
max_rows,
|
|
68
|
+
max_cols,
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
service = build_sheets_service(context.get_auth_token_or_empty())
|
|
73
|
+
|
|
74
|
+
data = get_spreadsheet_with_pagination(
|
|
75
|
+
service,
|
|
76
|
+
spreadsheet_id,
|
|
77
|
+
sheet_identifier,
|
|
78
|
+
sheet_identifier_type,
|
|
79
|
+
start_row,
|
|
80
|
+
start_col,
|
|
81
|
+
max_rows,
|
|
82
|
+
max_cols,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
raise_for_large_payload(data)
|
|
86
|
+
return data
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@tool(
|
|
90
|
+
requires_auth=Google(
|
|
91
|
+
scopes=["https://www.googleapis.com/auth/drive.file"],
|
|
92
|
+
),
|
|
93
|
+
requires_metadata=[ToolMetadataKey.CLIENT_ID, ToolMetadataKey.COORDINATOR_URL],
|
|
94
|
+
)
|
|
95
|
+
@with_filepicker_fallback
|
|
96
|
+
async def get_spreadsheet_metadata(
|
|
97
|
+
context: ToolContext,
|
|
98
|
+
spreadsheet_id: Annotated[str, "The id of the spreadsheet to get metadata for"],
|
|
99
|
+
) -> Annotated[dict, "The spreadsheet metadata for the specified spreadsheet"]:
|
|
100
|
+
"""Gets the metadata for a spreadsheet including the metadata for the sheets in the spreadsheet.
|
|
101
|
+
|
|
102
|
+
Use this tool to get the name, position, ID, and URL of all sheets in a spreadsheet as well as
|
|
103
|
+
the number of rows and columns in each sheet.
|
|
104
|
+
|
|
105
|
+
Does not return the content/data of the sheets in the spreadsheet - only the metadata.
|
|
106
|
+
Excludes spreadsheets that are in the trash.
|
|
30
107
|
"""
|
|
31
108
|
service = build_sheets_service(context.get_auth_token_or_empty())
|
|
32
109
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
110
|
+
metadata = get_spreadsheet_metadata_helper(service, spreadsheet_id)
|
|
111
|
+
metadata_dict = metadata.model_dump(exclude_none=True)
|
|
112
|
+
for sheet in metadata_dict.get("sheets", []):
|
|
113
|
+
sheet["sheet_url"] = sheet_url_template.format(
|
|
114
|
+
spreadsheet_id=spreadsheet_id,
|
|
115
|
+
sheet_id=sheet["properties"]["sheetId"],
|
|
39
116
|
)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"spreadsheet_title": metadata_dict["properties"]["title"],
|
|
120
|
+
"spreadsheet_id": metadata_dict["spreadsheetId"],
|
|
121
|
+
"spreadsheet_url": metadata_dict["spreadsheetUrl"],
|
|
122
|
+
"sheets": metadata_dict["sheets"],
|
|
123
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from typing import Annotated, Any
|
|
2
|
+
|
|
3
|
+
from arcade_tdk import ToolContext, ToolMetadataKey, tool
|
|
4
|
+
from arcade_tdk.auth import Google
|
|
5
|
+
|
|
6
|
+
from arcade_google_sheets.enums import OrderBy
|
|
7
|
+
from arcade_google_sheets.file_picker import generate_google_file_picker_url
|
|
8
|
+
from arcade_google_sheets.templates import (
|
|
9
|
+
optional_file_picker_instructions_template,
|
|
10
|
+
spreadsheet_url_template,
|
|
11
|
+
)
|
|
12
|
+
from arcade_google_sheets.utils import (
|
|
13
|
+
build_drive_service,
|
|
14
|
+
build_files_list_params,
|
|
15
|
+
remove_none_values,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@tool(
|
|
20
|
+
requires_auth=Google(
|
|
21
|
+
scopes=["https://www.googleapis.com/auth/drive.file"],
|
|
22
|
+
),
|
|
23
|
+
requires_metadata=[ToolMetadataKey.CLIENT_ID, ToolMetadataKey.COORDINATOR_URL],
|
|
24
|
+
)
|
|
25
|
+
async def search_spreadsheets(
|
|
26
|
+
context: ToolContext,
|
|
27
|
+
spreadsheet_contains: Annotated[
|
|
28
|
+
list[str] | None,
|
|
29
|
+
"Keywords or phrases that must be in the spreadsheet title. Provide a list of "
|
|
30
|
+
"keywords or phrases if needed.",
|
|
31
|
+
] = None,
|
|
32
|
+
spreadsheet_not_contains: Annotated[
|
|
33
|
+
list[str] | None,
|
|
34
|
+
"Keywords or phrases that must NOT be in the spreadsheet title. Provide a list of "
|
|
35
|
+
"keywords or phrases if needed.",
|
|
36
|
+
] = None,
|
|
37
|
+
search_only_in_shared_drive_id: Annotated[
|
|
38
|
+
str | None,
|
|
39
|
+
"The ID of the shared drive to restrict the search to. If provided, the search will only "
|
|
40
|
+
"return spreadsheets from this drive. Defaults to None, which searches across all drives.",
|
|
41
|
+
] = None,
|
|
42
|
+
include_shared_drives: Annotated[
|
|
43
|
+
bool,
|
|
44
|
+
"Whether to include spreadsheets from shared drives. Defaults to False (searches only in "
|
|
45
|
+
"the user's 'My Drive').",
|
|
46
|
+
] = False,
|
|
47
|
+
include_organization_domain_spreadsheets: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
"Whether to include spreadsheets from the organization's domain. "
|
|
50
|
+
"This is applicable to admin users who have permissions to view "
|
|
51
|
+
"organization-wide spreadsheets in a Google Workspace account. "
|
|
52
|
+
"Defaults to False.",
|
|
53
|
+
] = False,
|
|
54
|
+
order_by: Annotated[
|
|
55
|
+
list[OrderBy] | None,
|
|
56
|
+
"Sort order. Defaults to listing the most recently modified spreadsheets first",
|
|
57
|
+
] = None,
|
|
58
|
+
limit: Annotated[
|
|
59
|
+
int, "The maximum number of spreadsheets to list. Defaults to 10. Max is 50"
|
|
60
|
+
] = 10,
|
|
61
|
+
pagination_token: Annotated[
|
|
62
|
+
str | None, "The pagination token to continue a previous request"
|
|
63
|
+
] = None,
|
|
64
|
+
) -> Annotated[
|
|
65
|
+
dict,
|
|
66
|
+
"A dictionary containing the title, ID, and URL for each matching spreadsheet. "
|
|
67
|
+
"Also contains a pagination token if there are more spreadsheets to list.",
|
|
68
|
+
]:
|
|
69
|
+
"""
|
|
70
|
+
Searches for spreadsheets in the user's Google Drive based on the titles and content and
|
|
71
|
+
returns the title, ID, and URL for each matching spreadsheet.
|
|
72
|
+
|
|
73
|
+
Does not return the content/data of the sheets in the spreadsheets - only the metadata.
|
|
74
|
+
Excludes spreadsheets that are in the trash.
|
|
75
|
+
"""
|
|
76
|
+
if order_by is None:
|
|
77
|
+
order_by = [OrderBy.MODIFIED_TIME_DESC]
|
|
78
|
+
elif isinstance(order_by, OrderBy):
|
|
79
|
+
order_by = [order_by]
|
|
80
|
+
|
|
81
|
+
limit = max(1, min(50, limit))
|
|
82
|
+
page_size = min(10, limit)
|
|
83
|
+
spreadsheets: list[dict[str, Any]] = []
|
|
84
|
+
|
|
85
|
+
drive_service = build_drive_service(context.get_auth_token_or_empty())
|
|
86
|
+
|
|
87
|
+
params = build_files_list_params(
|
|
88
|
+
mime_type="application/vnd.google-apps.spreadsheet",
|
|
89
|
+
page_size=page_size,
|
|
90
|
+
order_by=order_by,
|
|
91
|
+
pagination_token=pagination_token,
|
|
92
|
+
include_shared_drives=include_shared_drives,
|
|
93
|
+
search_only_in_shared_drive_id=search_only_in_shared_drive_id,
|
|
94
|
+
include_organization_domain_spreadsheets=include_organization_domain_spreadsheets,
|
|
95
|
+
spreadsheet_contains=spreadsheet_contains,
|
|
96
|
+
spreadsheet_not_contains=spreadsheet_not_contains,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
while len(spreadsheets) < limit:
|
|
100
|
+
if pagination_token:
|
|
101
|
+
params["pageToken"] = pagination_token
|
|
102
|
+
else:
|
|
103
|
+
params.pop("pageToken", None)
|
|
104
|
+
|
|
105
|
+
results = drive_service.files().list(**params).execute()
|
|
106
|
+
batch = results.get("files", [])
|
|
107
|
+
spreadsheets.extend(batch[: limit - len(spreadsheets)])
|
|
108
|
+
|
|
109
|
+
pagination_token = results.get("nextPageToken")
|
|
110
|
+
if not pagination_token or len(batch) < page_size:
|
|
111
|
+
break
|
|
112
|
+
|
|
113
|
+
# Add the spreadsheet URL to each spreadsheet
|
|
114
|
+
for spreadsheet in spreadsheets:
|
|
115
|
+
spreadsheet["url"] = spreadsheet_url_template.format(spreadsheet_id=spreadsheet["id"])
|
|
116
|
+
|
|
117
|
+
file_picker_response = generate_google_file_picker_url(
|
|
118
|
+
context,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
tool_response = {
|
|
122
|
+
"pagination_token": pagination_token,
|
|
123
|
+
"spreadsheets_count": len(spreadsheets),
|
|
124
|
+
"spreadsheets": spreadsheets,
|
|
125
|
+
"file_picker": {
|
|
126
|
+
"url": file_picker_response["url"],
|
|
127
|
+
"llm_instructions": optional_file_picker_instructions_template.format(
|
|
128
|
+
url=file_picker_response["url"]
|
|
129
|
+
),
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
tool_response = remove_none_values(tool_response)
|
|
133
|
+
|
|
134
|
+
return tool_response
|