workspace-mcp 1.1.5__py3-none-any.whl → 1.1.7__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.
- core/utils.py +153 -54
- gcalendar/calendar_tools.py +6 -6
- gdocs/docs_tools.py +4 -4
- gdrive/drive_tools.py +5 -5
- gforms/forms_tools.py +5 -5
- gmail/gmail_tools.py +199 -81
- gsheets/sheets_tools.py +7 -7
- gslides/slides_tools.py +25 -25
- gtasks/tasks_tools.py +13 -0
- {workspace_mcp-1.1.5.dist-info → workspace_mcp-1.1.7.dist-info}/METADATA +14 -7
- {workspace_mcp-1.1.5.dist-info → workspace_mcp-1.1.7.dist-info}/RECORD +15 -15
- {workspace_mcp-1.1.5.dist-info → workspace_mcp-1.1.7.dist-info}/WHEEL +0 -0
- {workspace_mcp-1.1.5.dist-info → workspace_mcp-1.1.7.dist-info}/entry_points.txt +0 -0
- {workspace_mcp-1.1.5.dist-info → workspace_mcp-1.1.7.dist-info}/licenses/LICENSE +0 -0
- {workspace_mcp-1.1.5.dist-info → workspace_mcp-1.1.7.dist-info}/top_level.txt +0 -0
core/utils.py
CHANGED
@@ -3,11 +3,24 @@ import logging
|
|
3
3
|
import os
|
4
4
|
import tempfile
|
5
5
|
import zipfile, xml.etree.ElementTree as ET
|
6
|
+
import ssl
|
7
|
+
import time
|
8
|
+
import asyncio
|
9
|
+
import functools
|
6
10
|
|
7
11
|
from typing import List, Optional
|
8
12
|
|
13
|
+
from googleapiclient.errors import HttpError
|
14
|
+
|
9
15
|
logger = logging.getLogger(__name__)
|
10
16
|
|
17
|
+
|
18
|
+
class TransientNetworkError(Exception):
|
19
|
+
"""Custom exception for transient network errors after retries."""
|
20
|
+
|
21
|
+
pass
|
22
|
+
|
23
|
+
|
11
24
|
def check_credentials_directory_permissions(credentials_dir: str = None) -> None:
|
12
25
|
"""
|
13
26
|
Check if the service has appropriate permissions to create and write to the .credentials directory.
|
@@ -21,6 +34,7 @@ def check_credentials_directory_permissions(credentials_dir: str = None) -> None
|
|
21
34
|
"""
|
22
35
|
if credentials_dir is None:
|
23
36
|
from auth.google_auth import get_default_credentials_dir
|
37
|
+
|
24
38
|
credentials_dir = get_default_credentials_dir()
|
25
39
|
|
26
40
|
try:
|
@@ -29,22 +43,28 @@ def check_credentials_directory_permissions(credentials_dir: str = None) -> None
|
|
29
43
|
# Directory exists, check if we can write to it
|
30
44
|
test_file = os.path.join(credentials_dir, ".permission_test")
|
31
45
|
try:
|
32
|
-
with open(test_file,
|
46
|
+
with open(test_file, "w") as f:
|
33
47
|
f.write("test")
|
34
48
|
os.remove(test_file)
|
35
|
-
logger.info(
|
49
|
+
logger.info(
|
50
|
+
f"Credentials directory permissions check passed: {os.path.abspath(credentials_dir)}"
|
51
|
+
)
|
36
52
|
except (PermissionError, OSError) as e:
|
37
|
-
raise PermissionError(
|
53
|
+
raise PermissionError(
|
54
|
+
f"Cannot write to existing credentials directory '{os.path.abspath(credentials_dir)}': {e}"
|
55
|
+
)
|
38
56
|
else:
|
39
57
|
# Directory doesn't exist, try to create it and its parent directories
|
40
58
|
try:
|
41
59
|
os.makedirs(credentials_dir, exist_ok=True)
|
42
60
|
# Test writing to the new directory
|
43
61
|
test_file = os.path.join(credentials_dir, ".permission_test")
|
44
|
-
with open(test_file,
|
62
|
+
with open(test_file, "w") as f:
|
45
63
|
f.write("test")
|
46
64
|
os.remove(test_file)
|
47
|
-
logger.info(
|
65
|
+
logger.info(
|
66
|
+
f"Created credentials directory with proper permissions: {os.path.abspath(credentials_dir)}"
|
67
|
+
)
|
48
68
|
except (PermissionError, OSError) as e:
|
49
69
|
# Clean up if we created the directory but can't write to it
|
50
70
|
try:
|
@@ -52,12 +72,17 @@ def check_credentials_directory_permissions(credentials_dir: str = None) -> None
|
|
52
72
|
os.rmdir(credentials_dir)
|
53
73
|
except:
|
54
74
|
pass
|
55
|
-
raise PermissionError(
|
75
|
+
raise PermissionError(
|
76
|
+
f"Cannot create or write to credentials directory '{os.path.abspath(credentials_dir)}': {e}"
|
77
|
+
)
|
56
78
|
|
57
79
|
except PermissionError:
|
58
80
|
raise
|
59
81
|
except Exception as e:
|
60
|
-
raise OSError(
|
82
|
+
raise OSError(
|
83
|
+
f"Unexpected error checking credentials directory permissions: {e}"
|
84
|
+
)
|
85
|
+
|
61
86
|
|
62
87
|
def extract_office_xml_text(file_bytes: bytes, mime_type: str) -> Optional[str]:
|
63
88
|
"""
|
@@ -66,23 +91,38 @@ def extract_office_xml_text(file_bytes: bytes, mime_type: str) -> Optional[str]:
|
|
66
91
|
No external deps – just std-lib zipfile + ElementTree.
|
67
92
|
"""
|
68
93
|
shared_strings: List[str] = []
|
69
|
-
ns_excel_main =
|
94
|
+
ns_excel_main = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
70
95
|
|
71
96
|
try:
|
72
97
|
with zipfile.ZipFile(io.BytesIO(file_bytes)) as zf:
|
73
98
|
targets: List[str] = []
|
74
99
|
# Map MIME → iterable of XML files to inspect
|
75
|
-
if
|
100
|
+
if (
|
101
|
+
mime_type
|
102
|
+
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
103
|
+
):
|
76
104
|
targets = ["word/document.xml"]
|
77
|
-
elif
|
105
|
+
elif (
|
106
|
+
mime_type
|
107
|
+
== "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
108
|
+
):
|
78
109
|
targets = [n for n in zf.namelist() if n.startswith("ppt/slides/slide")]
|
79
|
-
elif
|
80
|
-
|
110
|
+
elif (
|
111
|
+
mime_type
|
112
|
+
== "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
113
|
+
):
|
114
|
+
targets = [
|
115
|
+
n
|
116
|
+
for n in zf.namelist()
|
117
|
+
if n.startswith("xl/worksheets/sheet") and "drawing" not in n
|
118
|
+
]
|
81
119
|
# Attempt to parse sharedStrings.xml for Excel files
|
82
120
|
try:
|
83
121
|
shared_strings_xml = zf.read("xl/sharedStrings.xml")
|
84
122
|
shared_strings_root = ET.fromstring(shared_strings_xml)
|
85
|
-
for si_element in shared_strings_root.findall(
|
123
|
+
for si_element in shared_strings_root.findall(
|
124
|
+
f"{{{ns_excel_main}}}si"
|
125
|
+
):
|
86
126
|
text_parts = []
|
87
127
|
# Find all <t> elements, simple or within <r> runs, and concatenate their text
|
88
128
|
for t_element in si_element.findall(f".//{{{ns_excel_main}}}t"):
|
@@ -90,11 +130,18 @@ def extract_office_xml_text(file_bytes: bytes, mime_type: str) -> Optional[str]:
|
|
90
130
|
text_parts.append(t_element.text)
|
91
131
|
shared_strings.append("".join(text_parts))
|
92
132
|
except KeyError:
|
93
|
-
logger.info(
|
133
|
+
logger.info(
|
134
|
+
"No sharedStrings.xml found in Excel file (this is optional)."
|
135
|
+
)
|
94
136
|
except ET.ParseError as e:
|
95
137
|
logger.error(f"Error parsing sharedStrings.xml: {e}")
|
96
|
-
except
|
97
|
-
|
138
|
+
except (
|
139
|
+
Exception
|
140
|
+
) as e: # Catch any other unexpected error during sharedStrings parsing
|
141
|
+
logger.error(
|
142
|
+
f"Unexpected error processing sharedStrings.xml: {e}",
|
143
|
+
exc_info=True,
|
144
|
+
)
|
98
145
|
else:
|
99
146
|
return None
|
100
147
|
|
@@ -105,93 +152,145 @@ def extract_office_xml_text(file_bytes: bytes, mime_type: str) -> Optional[str]:
|
|
105
152
|
xml_root = ET.fromstring(xml_content)
|
106
153
|
member_texts: List[str] = []
|
107
154
|
|
108
|
-
if
|
109
|
-
|
110
|
-
|
155
|
+
if (
|
156
|
+
mime_type
|
157
|
+
== "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
158
|
+
):
|
159
|
+
for cell_element in xml_root.findall(
|
160
|
+
f".//{{{ns_excel_main}}}c"
|
161
|
+
): # Find all <c> elements
|
162
|
+
value_element = cell_element.find(
|
163
|
+
f"{{{ns_excel_main}}}v"
|
164
|
+
) # Find <v> under <c>
|
111
165
|
|
112
166
|
# Skip if cell has no value element or value element has no text
|
113
167
|
if value_element is None or value_element.text is None:
|
114
168
|
continue
|
115
169
|
|
116
|
-
cell_type = cell_element.get(
|
117
|
-
if cell_type ==
|
170
|
+
cell_type = cell_element.get("t")
|
171
|
+
if cell_type == "s": # Shared string
|
118
172
|
try:
|
119
173
|
ss_idx = int(value_element.text)
|
120
174
|
if 0 <= ss_idx < len(shared_strings):
|
121
175
|
member_texts.append(shared_strings[ss_idx])
|
122
176
|
else:
|
123
|
-
logger.warning(
|
177
|
+
logger.warning(
|
178
|
+
f"Invalid shared string index {ss_idx} in {member}. Max index: {len(shared_strings)-1}"
|
179
|
+
)
|
124
180
|
except ValueError:
|
125
|
-
logger.warning(
|
181
|
+
logger.warning(
|
182
|
+
f"Non-integer shared string index: '{value_element.text}' in {member}."
|
183
|
+
)
|
126
184
|
else: # Direct value (number, boolean, inline string if not 's')
|
127
185
|
member_texts.append(value_element.text)
|
128
186
|
else: # Word or PowerPoint
|
129
187
|
for elem in xml_root.iter():
|
130
188
|
# For Word: <w:t> where w is "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
131
189
|
# For PowerPoint: <a:t> where a is "http://schemas.openxmlformats.org/drawingml/2006/main"
|
132
|
-
if
|
190
|
+
if (
|
191
|
+
elem.tag.endswith("}t") and elem.text
|
192
|
+
): # Check for any namespaced tag ending with 't'
|
133
193
|
cleaned_text = elem.text.strip()
|
134
|
-
if
|
135
|
-
|
194
|
+
if (
|
195
|
+
cleaned_text
|
196
|
+
): # Add only if there's non-whitespace text
|
197
|
+
member_texts.append(cleaned_text)
|
136
198
|
|
137
199
|
if member_texts:
|
138
|
-
pieces.append(
|
200
|
+
pieces.append(
|
201
|
+
" ".join(member_texts)
|
202
|
+
) # Join texts from one member with spaces
|
139
203
|
|
140
204
|
except ET.ParseError as e:
|
141
|
-
logger.warning(
|
205
|
+
logger.warning(
|
206
|
+
f"Could not parse XML in member '{member}' for {mime_type} file: {e}"
|
207
|
+
)
|
142
208
|
except Exception as e:
|
143
|
-
logger.error(
|
209
|
+
logger.error(
|
210
|
+
f"Error processing member '{member}' for {mime_type}: {e}",
|
211
|
+
exc_info=True,
|
212
|
+
)
|
144
213
|
# continue processing other members
|
145
214
|
|
146
|
-
if not pieces:
|
215
|
+
if not pieces: # If no text was extracted at all
|
147
216
|
return None
|
148
217
|
|
149
218
|
# Join content from different members (sheets/slides) with double newlines for separation
|
150
219
|
text = "\n\n".join(pieces).strip()
|
151
|
-
return text or None
|
220
|
+
return text or None # Ensure None is returned if text is empty after strip
|
152
221
|
|
153
222
|
except zipfile.BadZipFile:
|
154
223
|
logger.warning(f"File is not a valid ZIP archive (mime_type: {mime_type}).")
|
155
224
|
return None
|
156
|
-
except
|
225
|
+
except (
|
226
|
+
ET.ParseError
|
227
|
+
) as e: # Catch parsing errors at the top level if zipfile itself is XML-like
|
157
228
|
logger.error(f"XML parsing error at a high level for {mime_type}: {e}")
|
158
229
|
return None
|
159
230
|
except Exception as e:
|
160
|
-
logger.error(
|
231
|
+
logger.error(
|
232
|
+
f"Failed to extract office XML text for {mime_type}: {e}", exc_info=True
|
233
|
+
)
|
161
234
|
return None
|
162
235
|
|
163
|
-
import functools
|
164
|
-
from googleapiclient.errors import HttpError
|
165
236
|
|
166
|
-
def handle_http_errors(tool_name: str):
|
237
|
+
def handle_http_errors(tool_name: str, is_read_only: bool = False):
|
167
238
|
"""
|
168
|
-
A decorator to handle Google API HttpErrors in a standardized way.
|
239
|
+
A decorator to handle Google API HttpErrors and transient SSL errors in a standardized way.
|
169
240
|
|
170
241
|
It wraps a tool function, catches HttpError, logs a detailed error message,
|
171
242
|
and raises a generic Exception with a user-friendly message.
|
172
243
|
|
244
|
+
If is_read_only is True, it will also catch ssl.SSLError and retry with
|
245
|
+
exponential backoff. After exhausting retries, it raises a TransientNetworkError.
|
246
|
+
|
173
247
|
Args:
|
174
248
|
tool_name (str): The name of the tool being decorated (e.g., 'list_calendars').
|
175
|
-
|
249
|
+
is_read_only (bool): If True, the operation is considered safe to retry on
|
250
|
+
transient network errors. Defaults to False.
|
176
251
|
"""
|
252
|
+
|
177
253
|
def decorator(func):
|
178
254
|
@functools.wraps(func)
|
179
255
|
async def wrapper(*args, **kwargs):
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
256
|
+
max_retries = 3
|
257
|
+
base_delay = 1
|
258
|
+
|
259
|
+
for attempt in range(max_retries):
|
260
|
+
try:
|
261
|
+
return await func(*args, **kwargs)
|
262
|
+
except ssl.SSLError as e:
|
263
|
+
if is_read_only and attempt < max_retries - 1:
|
264
|
+
delay = base_delay * (2**attempt)
|
265
|
+
logger.warning(
|
266
|
+
f"SSL error in {tool_name} on attempt {attempt + 1}: {e}. Retrying in {delay} seconds..."
|
267
|
+
)
|
268
|
+
await asyncio.sleep(delay)
|
269
|
+
else:
|
270
|
+
logger.error(
|
271
|
+
f"SSL error in {tool_name} on final attempt: {e}. Raising exception."
|
272
|
+
)
|
273
|
+
raise TransientNetworkError(
|
274
|
+
f"A transient SSL error occurred in '{tool_name}' after {max_retries} attempts. "
|
275
|
+
"This is likely a temporary network or certificate issue. Please try again shortly."
|
276
|
+
) from e
|
277
|
+
except HttpError as error:
|
278
|
+
user_google_email = kwargs.get("user_google_email", "N/A")
|
279
|
+
message = (
|
280
|
+
f"API error in {tool_name}: {error}. "
|
281
|
+
f"You might need to re-authenticate for user '{user_google_email}'. "
|
282
|
+
f"LLM: Try 'start_google_auth' with the user's email and the appropriate service_name."
|
283
|
+
)
|
284
|
+
logger.error(message, exc_info=True)
|
285
|
+
raise Exception(message) from error
|
286
|
+
except TransientNetworkError:
|
287
|
+
# Re-raise without wrapping to preserve the specific error type
|
288
|
+
raise
|
289
|
+
except Exception as e:
|
290
|
+
message = f"An unexpected error occurred in {tool_name}: {e}"
|
291
|
+
logger.exception(message)
|
292
|
+
raise Exception(message) from e
|
293
|
+
|
196
294
|
return wrapper
|
295
|
+
|
197
296
|
return decorator
|
gcalendar/calendar_tools.py
CHANGED
@@ -80,8 +80,8 @@ def _correct_time_format_for_api(
|
|
80
80
|
|
81
81
|
|
82
82
|
@server.tool()
|
83
|
+
@handle_http_errors("list_calendars", is_read_only=True)
|
83
84
|
@require_google_service("calendar", "calendar_read")
|
84
|
-
@handle_http_errors("list_calendars")
|
85
85
|
async def list_calendars(service, user_google_email: str) -> str:
|
86
86
|
"""
|
87
87
|
Retrieves a list of calendars accessible to the authenticated user.
|
@@ -114,8 +114,8 @@ async def list_calendars(service, user_google_email: str) -> str:
|
|
114
114
|
|
115
115
|
|
116
116
|
@server.tool()
|
117
|
+
@handle_http_errors("get_events", is_read_only=True)
|
117
118
|
@require_google_service("calendar", "calendar_read")
|
118
|
-
@handle_http_errors("get_events")
|
119
119
|
async def get_events(
|
120
120
|
service,
|
121
121
|
user_google_email: str,
|
@@ -202,8 +202,8 @@ async def get_events(
|
|
202
202
|
|
203
203
|
|
204
204
|
@server.tool()
|
205
|
-
@require_google_service("calendar", "calendar_events")
|
206
205
|
@handle_http_errors("create_event")
|
206
|
+
@require_google_service("calendar", "calendar_events")
|
207
207
|
async def create_event(
|
208
208
|
service,
|
209
209
|
user_google_email: str,
|
@@ -326,8 +326,8 @@ async def create_event(
|
|
326
326
|
|
327
327
|
|
328
328
|
@server.tool()
|
329
|
-
@require_google_service("calendar", "calendar_events")
|
330
329
|
@handle_http_errors("modify_event")
|
330
|
+
@require_google_service("calendar", "calendar_events")
|
331
331
|
async def modify_event(
|
332
332
|
service,
|
333
333
|
user_google_email: str,
|
@@ -446,8 +446,8 @@ async def modify_event(
|
|
446
446
|
|
447
447
|
|
448
448
|
@server.tool()
|
449
|
-
@require_google_service("calendar", "calendar_events")
|
450
449
|
@handle_http_errors("delete_event")
|
450
|
+
@require_google_service("calendar", "calendar_events")
|
451
451
|
async def delete_event(service, user_google_email: str, event_id: str, calendar_id: str = "primary") -> str:
|
452
452
|
"""
|
453
453
|
Deletes an existing event.
|
@@ -500,8 +500,8 @@ async def delete_event(service, user_google_email: str, event_id: str, calendar_
|
|
500
500
|
|
501
501
|
|
502
502
|
@server.tool()
|
503
|
+
@handle_http_errors("get_event", is_read_only=True)
|
503
504
|
@require_google_service("calendar", "calendar_read")
|
504
|
-
@handle_http_errors("get_event")
|
505
505
|
async def get_event(
|
506
506
|
service,
|
507
507
|
user_google_email: str,
|
gdocs/docs_tools.py
CHANGED
@@ -20,8 +20,8 @@ from core.comments import create_comment_tools
|
|
20
20
|
logger = logging.getLogger(__name__)
|
21
21
|
|
22
22
|
@server.tool()
|
23
|
+
@handle_http_errors("search_docs", is_read_only=True)
|
23
24
|
@require_google_service("drive", "drive_read")
|
24
|
-
@handle_http_errors("search_docs")
|
25
25
|
async def search_docs(
|
26
26
|
service,
|
27
27
|
user_google_email: str,
|
@@ -57,11 +57,11 @@ async def search_docs(
|
|
57
57
|
return "\n".join(output)
|
58
58
|
|
59
59
|
@server.tool()
|
60
|
+
@handle_http_errors("get_doc_content", is_read_only=True)
|
60
61
|
@require_multiple_services([
|
61
62
|
{"service_type": "drive", "scopes": "drive_read", "param_name": "drive_service"},
|
62
63
|
{"service_type": "docs", "scopes": "docs_read", "param_name": "docs_service"}
|
63
64
|
])
|
64
|
-
@handle_http_errors("get_doc_content")
|
65
65
|
async def get_doc_content(
|
66
66
|
drive_service,
|
67
67
|
docs_service,
|
@@ -157,8 +157,8 @@ async def get_doc_content(
|
|
157
157
|
return header + body_text
|
158
158
|
|
159
159
|
@server.tool()
|
160
|
+
@handle_http_errors("list_docs_in_folder", is_read_only=True)
|
160
161
|
@require_google_service("drive", "drive_read")
|
161
|
-
@handle_http_errors("list_docs_in_folder")
|
162
162
|
async def list_docs_in_folder(
|
163
163
|
service,
|
164
164
|
user_google_email: str,
|
@@ -189,8 +189,8 @@ async def list_docs_in_folder(
|
|
189
189
|
return "\n".join(out)
|
190
190
|
|
191
191
|
@server.tool()
|
192
|
-
@require_google_service("docs", "docs_write")
|
193
192
|
@handle_http_errors("create_doc")
|
193
|
+
@require_google_service("docs", "docs_write")
|
194
194
|
async def create_doc(
|
195
195
|
service,
|
196
196
|
user_google_email: str,
|
gdrive/drive_tools.py
CHANGED
@@ -76,8 +76,8 @@ def _build_drive_list_params(
|
|
76
76
|
return list_params
|
77
77
|
|
78
78
|
@server.tool()
|
79
|
+
@handle_http_errors("search_drive_files", is_read_only=True)
|
79
80
|
@require_google_service("drive", "drive_read")
|
80
|
-
@handle_http_errors("search_drive_files")
|
81
81
|
async def search_drive_files(
|
82
82
|
service,
|
83
83
|
user_google_email: str,
|
@@ -143,8 +143,8 @@ async def search_drive_files(
|
|
143
143
|
return text_output
|
144
144
|
|
145
145
|
@server.tool()
|
146
|
+
@handle_http_errors("get_drive_file_content", is_read_only=True)
|
146
147
|
@require_google_service("drive", "drive_read")
|
147
|
-
@handle_http_errors("get_drive_file_content")
|
148
148
|
async def get_drive_file_content(
|
149
149
|
service,
|
150
150
|
user_google_email: str,
|
@@ -200,7 +200,7 @@ async def get_drive_file_content(
|
|
200
200
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
201
201
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
202
202
|
}
|
203
|
-
|
203
|
+
|
204
204
|
if mime_type in office_mime_types:
|
205
205
|
office_text = extract_office_xml_text(file_content_bytes, mime_type)
|
206
206
|
if office_text:
|
@@ -233,8 +233,8 @@ async def get_drive_file_content(
|
|
233
233
|
|
234
234
|
|
235
235
|
@server.tool()
|
236
|
+
@handle_http_errors("list_drive_items", is_read_only=True)
|
236
237
|
@require_google_service("drive", "drive_read")
|
237
|
-
@handle_http_errors("list_drive_items")
|
238
238
|
async def list_drive_items(
|
239
239
|
service,
|
240
240
|
user_google_email: str,
|
@@ -289,8 +289,8 @@ async def list_drive_items(
|
|
289
289
|
return text_output
|
290
290
|
|
291
291
|
@server.tool()
|
292
|
-
@require_google_service("drive", "drive_file")
|
293
292
|
@handle_http_errors("create_drive_file")
|
293
|
+
@require_google_service("drive", "drive_file")
|
294
294
|
async def create_drive_file(
|
295
295
|
service,
|
296
296
|
user_google_email: str,
|
gforms/forms_tools.py
CHANGED
@@ -18,8 +18,8 @@ logger = logging.getLogger(__name__)
|
|
18
18
|
|
19
19
|
|
20
20
|
@server.tool()
|
21
|
-
@require_google_service("forms", "forms")
|
22
21
|
@handle_http_errors("create_form")
|
22
|
+
@require_google_service("forms", "forms")
|
23
23
|
async def create_form(
|
24
24
|
service,
|
25
25
|
user_google_email: str,
|
@@ -67,8 +67,8 @@ async def create_form(
|
|
67
67
|
|
68
68
|
|
69
69
|
@server.tool()
|
70
|
+
@handle_http_errors("get_form", is_read_only=True)
|
70
71
|
@require_google_service("forms", "forms")
|
71
|
-
@handle_http_errors("get_form")
|
72
72
|
async def get_form(
|
73
73
|
service,
|
74
74
|
user_google_email: str,
|
@@ -123,8 +123,8 @@ async def get_form(
|
|
123
123
|
|
124
124
|
|
125
125
|
@server.tool()
|
126
|
-
@require_google_service("forms", "forms")
|
127
126
|
@handle_http_errors("set_publish_settings")
|
127
|
+
@require_google_service("forms", "forms")
|
128
128
|
async def set_publish_settings(
|
129
129
|
service,
|
130
130
|
user_google_email: str,
|
@@ -161,8 +161,8 @@ async def set_publish_settings(
|
|
161
161
|
|
162
162
|
|
163
163
|
@server.tool()
|
164
|
+
@handle_http_errors("get_form_response", is_read_only=True)
|
164
165
|
@require_google_service("forms", "forms")
|
165
|
-
@handle_http_errors("get_form_response")
|
166
166
|
async def get_form_response(
|
167
167
|
service,
|
168
168
|
user_google_email: str,
|
@@ -215,8 +215,8 @@ async def get_form_response(
|
|
215
215
|
|
216
216
|
|
217
217
|
@server.tool()
|
218
|
+
@handle_http_errors("list_form_responses", is_read_only=True)
|
218
219
|
@require_google_service("forms", "forms")
|
219
|
-
@handle_http_errors("list_form_responses")
|
220
220
|
async def list_form_responses(
|
221
221
|
service,
|
222
222
|
user_google_email: str,
|