google-workspace-mcp 1.0.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.
- google_workspace_mcp/__init__.py +3 -0
- google_workspace_mcp/__main__.py +43 -0
- google_workspace_mcp/app.py +8 -0
- google_workspace_mcp/auth/__init__.py +7 -0
- google_workspace_mcp/auth/gauth.py +62 -0
- google_workspace_mcp/config.py +60 -0
- google_workspace_mcp/prompts/__init__.py +3 -0
- google_workspace_mcp/prompts/calendar.py +36 -0
- google_workspace_mcp/prompts/drive.py +18 -0
- google_workspace_mcp/prompts/gmail.py +65 -0
- google_workspace_mcp/prompts/slides.py +40 -0
- google_workspace_mcp/resources/__init__.py +13 -0
- google_workspace_mcp/resources/calendar.py +79 -0
- google_workspace_mcp/resources/drive.py +93 -0
- google_workspace_mcp/resources/gmail.py +58 -0
- google_workspace_mcp/resources/sheets_resources.py +92 -0
- google_workspace_mcp/resources/slides.py +421 -0
- google_workspace_mcp/services/__init__.py +21 -0
- google_workspace_mcp/services/base.py +73 -0
- google_workspace_mcp/services/calendar.py +256 -0
- google_workspace_mcp/services/docs_service.py +388 -0
- google_workspace_mcp/services/drive.py +454 -0
- google_workspace_mcp/services/gmail.py +676 -0
- google_workspace_mcp/services/sheets_service.py +466 -0
- google_workspace_mcp/services/slides.py +959 -0
- google_workspace_mcp/tools/__init__.py +7 -0
- google_workspace_mcp/tools/calendar.py +229 -0
- google_workspace_mcp/tools/docs_tools.py +277 -0
- google_workspace_mcp/tools/drive.py +221 -0
- google_workspace_mcp/tools/gmail.py +344 -0
- google_workspace_mcp/tools/sheets_tools.py +322 -0
- google_workspace_mcp/tools/slides.py +478 -0
- google_workspace_mcp/utils/__init__.py +1 -0
- google_workspace_mcp/utils/markdown_slides.py +504 -0
- google_workspace_mcp-1.0.0.dist-info/METADATA +547 -0
- google_workspace_mcp-1.0.0.dist-info/RECORD +38 -0
- google_workspace_mcp-1.0.0.dist-info/WHEEL +4 -0
- google_workspace_mcp-1.0.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,959 @@
|
|
1
|
+
"""
|
2
|
+
Google Slides service implementation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import re
|
8
|
+
from typing import Any
|
9
|
+
|
10
|
+
from markdowndeck import create_presentation
|
11
|
+
|
12
|
+
from google_workspace_mcp.auth import gauth
|
13
|
+
from google_workspace_mcp.services.base import BaseGoogleService
|
14
|
+
from google_workspace_mcp.utils.markdown_slides import MarkdownSlidesConverter
|
15
|
+
|
16
|
+
logger = logging.getLogger(__name__)
|
17
|
+
|
18
|
+
|
19
|
+
class SlidesService(BaseGoogleService):
|
20
|
+
"""
|
21
|
+
Service for interacting with Google Slides API.
|
22
|
+
"""
|
23
|
+
|
24
|
+
def __init__(self):
|
25
|
+
"""Initialize the Slides service."""
|
26
|
+
super().__init__("slides", "v1")
|
27
|
+
self.markdown_converter = MarkdownSlidesConverter()
|
28
|
+
|
29
|
+
def get_presentation(self, presentation_id: str) -> dict[str, Any]:
|
30
|
+
"""
|
31
|
+
Get a presentation by ID with its metadata and content.
|
32
|
+
|
33
|
+
Args:
|
34
|
+
presentation_id: The ID of the presentation to retrieve
|
35
|
+
|
36
|
+
Returns:
|
37
|
+
Presentation data dictionary or error information
|
38
|
+
"""
|
39
|
+
try:
|
40
|
+
return self.service.presentations().get(presentationId=presentation_id).execute()
|
41
|
+
except Exception as e:
|
42
|
+
return self.handle_api_error("get_presentation", e)
|
43
|
+
|
44
|
+
def create_presentation(self, title: str) -> dict[str, Any]:
|
45
|
+
"""
|
46
|
+
Create a new presentation with a title.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
title: The title of the new presentation
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
Created presentation data or error information
|
53
|
+
"""
|
54
|
+
try:
|
55
|
+
body = {"title": title}
|
56
|
+
return self.service.presentations().create(body=body).execute()
|
57
|
+
except Exception as e:
|
58
|
+
return self.handle_api_error("create_presentation", e)
|
59
|
+
|
60
|
+
def create_slide(self, presentation_id: str, layout: str = "TITLE_AND_BODY") -> dict[str, Any]:
|
61
|
+
"""
|
62
|
+
Add a new slide to an existing presentation.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
presentation_id: The ID of the presentation
|
66
|
+
layout: The layout type for the new slide
|
67
|
+
(e.g., 'TITLE_AND_BODY', 'TITLE_ONLY', 'BLANK')
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
Response data or error information
|
71
|
+
"""
|
72
|
+
try:
|
73
|
+
# Define the slide creation request
|
74
|
+
requests = [
|
75
|
+
{
|
76
|
+
"createSlide": {
|
77
|
+
"slideLayoutReference": {"predefinedLayout": layout},
|
78
|
+
"placeholderIdMappings": [],
|
79
|
+
}
|
80
|
+
}
|
81
|
+
]
|
82
|
+
|
83
|
+
logger.info(f"Sending API request to create slide: {json.dumps(requests[0], indent=2)}")
|
84
|
+
|
85
|
+
# Execute the request
|
86
|
+
response = (
|
87
|
+
self.service.presentations().batchUpdate(presentationId=presentation_id, body={"requests": requests}).execute()
|
88
|
+
)
|
89
|
+
|
90
|
+
logger.info(f"API response: {json.dumps(response, indent=2)}")
|
91
|
+
|
92
|
+
# Return information about the created slide
|
93
|
+
if "replies" in response and len(response["replies"]) > 0:
|
94
|
+
slide_id = response["replies"][0]["createSlide"]["objectId"]
|
95
|
+
return {
|
96
|
+
"presentationId": presentation_id,
|
97
|
+
"slideId": slide_id,
|
98
|
+
"layout": layout,
|
99
|
+
}
|
100
|
+
return response
|
101
|
+
except Exception as e:
|
102
|
+
return self.handle_api_error("create_slide", e)
|
103
|
+
|
104
|
+
def add_text(
|
105
|
+
self,
|
106
|
+
presentation_id: str,
|
107
|
+
slide_id: str,
|
108
|
+
text: str,
|
109
|
+
shape_type: str = "TEXT_BOX",
|
110
|
+
position: tuple[float, float] = (100, 100),
|
111
|
+
size: tuple[float, float] = (400, 100),
|
112
|
+
) -> dict[str, Any]:
|
113
|
+
"""
|
114
|
+
Add text to a slide by creating a text box.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
presentation_id: The ID of the presentation
|
118
|
+
slide_id: The ID of the slide
|
119
|
+
text: The text content to add
|
120
|
+
shape_type: The type of shape for the text (default is TEXT_BOX)
|
121
|
+
position: Tuple of (x, y) coordinates for position
|
122
|
+
size: Tuple of (width, height) for the text box
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
Response data or error information
|
126
|
+
"""
|
127
|
+
try:
|
128
|
+
# Create a unique element ID
|
129
|
+
element_id = f"text_{slide_id}_{hash(text) % 10000}"
|
130
|
+
|
131
|
+
# Define the text insertion requests
|
132
|
+
requests = [
|
133
|
+
# First create the shape
|
134
|
+
{
|
135
|
+
"createShape": {
|
136
|
+
"objectId": element_id, # Important: Include the objectId here
|
137
|
+
"shapeType": shape_type,
|
138
|
+
"elementProperties": {
|
139
|
+
"pageObjectId": slide_id,
|
140
|
+
"size": {
|
141
|
+
"width": {"magnitude": size[0], "unit": "PT"},
|
142
|
+
"height": {"magnitude": size[1], "unit": "PT"},
|
143
|
+
},
|
144
|
+
"transform": {
|
145
|
+
"scaleX": 1,
|
146
|
+
"scaleY": 1,
|
147
|
+
"translateX": position[0],
|
148
|
+
"translateY": position[1],
|
149
|
+
"unit": "PT",
|
150
|
+
},
|
151
|
+
},
|
152
|
+
}
|
153
|
+
},
|
154
|
+
# Then insert text into the shape
|
155
|
+
{
|
156
|
+
"insertText": {
|
157
|
+
"objectId": element_id,
|
158
|
+
"insertionIndex": 0,
|
159
|
+
"text": text,
|
160
|
+
}
|
161
|
+
},
|
162
|
+
]
|
163
|
+
|
164
|
+
logger.info(f"Sending API request to create shape: {json.dumps(requests[0], indent=2)}")
|
165
|
+
logger.info(f"Sending API request to insert text: {json.dumps(requests[1], indent=2)}")
|
166
|
+
|
167
|
+
# Execute the request
|
168
|
+
response = (
|
169
|
+
self.service.presentations().batchUpdate(presentationId=presentation_id, body={"requests": requests}).execute()
|
170
|
+
)
|
171
|
+
|
172
|
+
logger.info(f"API response: {json.dumps(response, indent=2)}")
|
173
|
+
|
174
|
+
return {
|
175
|
+
"presentationId": presentation_id,
|
176
|
+
"slideId": slide_id,
|
177
|
+
"elementId": element_id,
|
178
|
+
"operation": "add_text",
|
179
|
+
"result": "success",
|
180
|
+
}
|
181
|
+
except Exception as e:
|
182
|
+
return self.handle_api_error("add_text", e)
|
183
|
+
|
184
|
+
def add_formatted_text(
|
185
|
+
self,
|
186
|
+
presentation_id: str,
|
187
|
+
slide_id: str,
|
188
|
+
formatted_text: str,
|
189
|
+
shape_type: str = "TEXT_BOX",
|
190
|
+
position: tuple[float, float] = (100, 100),
|
191
|
+
size: tuple[float, float] = (400, 100),
|
192
|
+
) -> dict[str, Any]:
|
193
|
+
"""
|
194
|
+
Add rich-formatted text to a slide with styling.
|
195
|
+
|
196
|
+
Args:
|
197
|
+
presentation_id: The ID of the presentation
|
198
|
+
slide_id: The ID of the slide
|
199
|
+
formatted_text: Text with formatting markers (**, *, etc.)
|
200
|
+
shape_type: The type of shape for the text (default is TEXT_BOX)
|
201
|
+
position: Tuple of (x, y) coordinates for position
|
202
|
+
size: Tuple of (width, height) for the text box
|
203
|
+
|
204
|
+
Returns:
|
205
|
+
Response data or error information
|
206
|
+
"""
|
207
|
+
try:
|
208
|
+
logger.info(f"Adding formatted text to slide {slide_id}, position={position}, size={size}")
|
209
|
+
logger.info(f"Text content: '{formatted_text[:100]}...'")
|
210
|
+
logger.info(
|
211
|
+
f"Checking for formatting: bold={'**' in formatted_text}, italic={'*' in formatted_text}, code={'`' in formatted_text}"
|
212
|
+
)
|
213
|
+
|
214
|
+
# Create a unique element ID
|
215
|
+
element_id = f"text_{slide_id}_{hash(formatted_text) % 10000}"
|
216
|
+
|
217
|
+
# First create the text box
|
218
|
+
create_requests = [
|
219
|
+
# Create the shape
|
220
|
+
{
|
221
|
+
"createShape": {
|
222
|
+
"objectId": element_id, # FIX: Include the objectId
|
223
|
+
"shapeType": shape_type,
|
224
|
+
"elementProperties": {
|
225
|
+
"pageObjectId": slide_id,
|
226
|
+
"size": {
|
227
|
+
"width": {"magnitude": size[0], "unit": "PT"},
|
228
|
+
"height": {"magnitude": size[1], "unit": "PT"},
|
229
|
+
},
|
230
|
+
"transform": {
|
231
|
+
"scaleX": 1,
|
232
|
+
"scaleY": 1,
|
233
|
+
"translateX": position[0],
|
234
|
+
"translateY": position[1],
|
235
|
+
"unit": "PT",
|
236
|
+
},
|
237
|
+
},
|
238
|
+
}
|
239
|
+
}
|
240
|
+
]
|
241
|
+
|
242
|
+
# Log the shape creation request
|
243
|
+
logger.info(f"Sending API request to create shape: {json.dumps(create_requests[0], indent=2)}")
|
244
|
+
|
245
|
+
# Execute creation request
|
246
|
+
creation_response = (
|
247
|
+
self.service.presentations()
|
248
|
+
.batchUpdate(presentationId=presentation_id, body={"requests": create_requests})
|
249
|
+
.execute()
|
250
|
+
)
|
251
|
+
|
252
|
+
# Log the response
|
253
|
+
logger.info(f"API response for shape creation: {json.dumps(creation_response, indent=2)}")
|
254
|
+
|
255
|
+
# Process the formatted text
|
256
|
+
# First, remove formatting markers to get plain text
|
257
|
+
plain_text = formatted_text
|
258
|
+
# Remove bold markers
|
259
|
+
plain_text = re.sub(r"\*\*(.*?)\*\*", r"\1", plain_text)
|
260
|
+
# Remove italic markers
|
261
|
+
plain_text = re.sub(r"\*(.*?)\*", r"\1", plain_text)
|
262
|
+
# Remove code markers if present
|
263
|
+
plain_text = re.sub(r"`(.*?)`", r"\1", plain_text)
|
264
|
+
|
265
|
+
# Insert the plain text
|
266
|
+
text_request = [
|
267
|
+
{
|
268
|
+
"insertText": {
|
269
|
+
"objectId": element_id,
|
270
|
+
"insertionIndex": 0,
|
271
|
+
"text": plain_text,
|
272
|
+
}
|
273
|
+
}
|
274
|
+
]
|
275
|
+
|
276
|
+
# Log the text insertion request
|
277
|
+
logger.info(f"Sending API request to insert plain text: {json.dumps(text_request[0], indent=2)}")
|
278
|
+
|
279
|
+
# Execute text insertion
|
280
|
+
text_response = (
|
281
|
+
self.service.presentations()
|
282
|
+
.batchUpdate(
|
283
|
+
presentationId=presentation_id,
|
284
|
+
body={"requests": text_request},
|
285
|
+
)
|
286
|
+
.execute()
|
287
|
+
)
|
288
|
+
|
289
|
+
# Log the response
|
290
|
+
logger.info(f"API response for plain text insertion: {json.dumps(text_response, indent=2)}")
|
291
|
+
|
292
|
+
# Now generate style requests if there's formatting to apply
|
293
|
+
if "**" in formatted_text or "*" in formatted_text:
|
294
|
+
style_requests = []
|
295
|
+
|
296
|
+
# Process bold text
|
297
|
+
bold_pattern = r"\*\*(.*?)\*\*"
|
298
|
+
bold_matches = list(re.finditer(bold_pattern, formatted_text))
|
299
|
+
|
300
|
+
for match in bold_matches:
|
301
|
+
content = match.group(1)
|
302
|
+
|
303
|
+
# Find where this content appears in the plain text
|
304
|
+
start_pos = plain_text.find(content)
|
305
|
+
if start_pos >= 0: # Found the text
|
306
|
+
end_pos = start_pos + len(content)
|
307
|
+
|
308
|
+
# Create style request for bold
|
309
|
+
style_requests.append(
|
310
|
+
{
|
311
|
+
"updateTextStyle": {
|
312
|
+
"objectId": element_id,
|
313
|
+
"textRange": {
|
314
|
+
"startIndex": start_pos,
|
315
|
+
"endIndex": end_pos,
|
316
|
+
},
|
317
|
+
"style": {"bold": True},
|
318
|
+
"fields": "bold",
|
319
|
+
}
|
320
|
+
}
|
321
|
+
)
|
322
|
+
|
323
|
+
# Process italic text (making sure not to process text inside bold markers)
|
324
|
+
italic_pattern = r"\*(.*?)\*"
|
325
|
+
italic_matches = list(re.finditer(italic_pattern, formatted_text))
|
326
|
+
|
327
|
+
for match in italic_matches:
|
328
|
+
# Skip if this is part of a bold marker
|
329
|
+
is_part_of_bold = False
|
330
|
+
match_start = match.start()
|
331
|
+
match_end = match.end()
|
332
|
+
|
333
|
+
for bold_match in bold_matches:
|
334
|
+
bold_start = bold_match.start()
|
335
|
+
bold_end = bold_match.end()
|
336
|
+
if bold_start <= match_start and match_end <= bold_end:
|
337
|
+
is_part_of_bold = True
|
338
|
+
break
|
339
|
+
|
340
|
+
if not is_part_of_bold:
|
341
|
+
content = match.group(1)
|
342
|
+
|
343
|
+
# Find where this content appears in the plain text
|
344
|
+
start_pos = plain_text.find(content)
|
345
|
+
if start_pos >= 0: # Found the text
|
346
|
+
end_pos = start_pos + len(content)
|
347
|
+
|
348
|
+
# Create style request for italic
|
349
|
+
style_requests.append(
|
350
|
+
{
|
351
|
+
"updateTextStyle": {
|
352
|
+
"objectId": element_id,
|
353
|
+
"textRange": {
|
354
|
+
"startIndex": start_pos,
|
355
|
+
"endIndex": end_pos,
|
356
|
+
},
|
357
|
+
"style": {"italic": True},
|
358
|
+
"fields": "italic",
|
359
|
+
}
|
360
|
+
}
|
361
|
+
)
|
362
|
+
|
363
|
+
# Apply all style requests if we have any
|
364
|
+
if style_requests:
|
365
|
+
try:
|
366
|
+
# Log the style requests
|
367
|
+
logger.info(f"Sending API request to apply text styling with {len(style_requests)} style requests")
|
368
|
+
for i, req in enumerate(style_requests):
|
369
|
+
logger.info(f"Style request {i + 1}: {json.dumps(req, indent=2)}")
|
370
|
+
|
371
|
+
# Execute style requests
|
372
|
+
style_response = (
|
373
|
+
self.service.presentations()
|
374
|
+
.batchUpdate(
|
375
|
+
presentationId=presentation_id,
|
376
|
+
body={"requests": style_requests},
|
377
|
+
)
|
378
|
+
.execute()
|
379
|
+
)
|
380
|
+
|
381
|
+
# Log the response
|
382
|
+
logger.info(f"API response for text styling: {json.dumps(style_response, indent=2)}")
|
383
|
+
except Exception as style_error:
|
384
|
+
logger.warning(f"Failed to apply text styles: {str(style_error)}")
|
385
|
+
logger.exception("Style application error details")
|
386
|
+
|
387
|
+
return {
|
388
|
+
"presentationId": presentation_id,
|
389
|
+
"slideId": slide_id,
|
390
|
+
"elementId": element_id,
|
391
|
+
"operation": "add_formatted_text",
|
392
|
+
"result": "success",
|
393
|
+
}
|
394
|
+
except Exception as e:
|
395
|
+
return self.handle_api_error("add_formatted_text", e)
|
396
|
+
|
397
|
+
def add_bulleted_list(
|
398
|
+
self,
|
399
|
+
presentation_id: str,
|
400
|
+
slide_id: str,
|
401
|
+
items: list[str],
|
402
|
+
position: tuple[float, float] = (100, 100),
|
403
|
+
size: tuple[float, float] = (400, 200),
|
404
|
+
) -> dict[str, Any]:
|
405
|
+
"""
|
406
|
+
Add a bulleted list to a slide.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
presentation_id: The ID of the presentation
|
410
|
+
slide_id: The ID of the slide
|
411
|
+
items: List of bullet point text items
|
412
|
+
position: Tuple of (x, y) coordinates for position
|
413
|
+
size: Tuple of (width, height) for the text box
|
414
|
+
|
415
|
+
Returns:
|
416
|
+
Response data or error information
|
417
|
+
"""
|
418
|
+
try:
|
419
|
+
# Create a unique element ID
|
420
|
+
element_id = f"list_{slide_id}_{hash(str(items)) % 10000}"
|
421
|
+
|
422
|
+
# Prepare the text content with newlines
|
423
|
+
text_content = "\n".join(items)
|
424
|
+
|
425
|
+
# Log the request
|
426
|
+
log_data = {
|
427
|
+
"createShape": {
|
428
|
+
"objectId": element_id, # Include objectId here
|
429
|
+
"shapeType": "TEXT_BOX",
|
430
|
+
"elementProperties": {
|
431
|
+
"pageObjectId": slide_id,
|
432
|
+
"size": {
|
433
|
+
"width": {"magnitude": size[0]},
|
434
|
+
"height": {"magnitude": size[1]},
|
435
|
+
},
|
436
|
+
"transform": {
|
437
|
+
"translateX": position[0],
|
438
|
+
"translateY": position[1],
|
439
|
+
},
|
440
|
+
},
|
441
|
+
}
|
442
|
+
}
|
443
|
+
logger.info(f"Sending API request to create shape for bullet list: {json.dumps(log_data, indent=2)}")
|
444
|
+
|
445
|
+
# Create requests
|
446
|
+
requests = [
|
447
|
+
# First create the shape
|
448
|
+
{
|
449
|
+
"createShape": {
|
450
|
+
"objectId": element_id, # Include objectId here
|
451
|
+
"shapeType": "TEXT_BOX",
|
452
|
+
"elementProperties": {
|
453
|
+
"pageObjectId": slide_id,
|
454
|
+
"size": {
|
455
|
+
"width": {"magnitude": size[0], "unit": "PT"},
|
456
|
+
"height": {"magnitude": size[1], "unit": "PT"},
|
457
|
+
},
|
458
|
+
"transform": {
|
459
|
+
"scaleX": 1,
|
460
|
+
"scaleY": 1,
|
461
|
+
"translateX": position[0],
|
462
|
+
"translateY": position[1],
|
463
|
+
"unit": "PT",
|
464
|
+
},
|
465
|
+
},
|
466
|
+
}
|
467
|
+
},
|
468
|
+
# Insert the text content
|
469
|
+
{
|
470
|
+
"insertText": {
|
471
|
+
"objectId": element_id,
|
472
|
+
"insertionIndex": 0,
|
473
|
+
"text": text_content,
|
474
|
+
}
|
475
|
+
},
|
476
|
+
]
|
477
|
+
|
478
|
+
# Log the text insertion
|
479
|
+
logger.info(f"Sending API request to insert bullet text: {json.dumps(requests[1], indent=2)}")
|
480
|
+
|
481
|
+
# Execute the request to create shape and insert text
|
482
|
+
response = (
|
483
|
+
self.service.presentations().batchUpdate(presentationId=presentation_id, body={"requests": requests}).execute()
|
484
|
+
)
|
485
|
+
|
486
|
+
# Log the response
|
487
|
+
logger.info(f"API response for bullet list creation: {json.dumps(response, indent=2)}")
|
488
|
+
|
489
|
+
# Now add bullet formatting
|
490
|
+
try:
|
491
|
+
# Use a simpler approach - apply bullets to the whole shape
|
492
|
+
bullet_request = [
|
493
|
+
{
|
494
|
+
"createParagraphBullets": {
|
495
|
+
"objectId": element_id,
|
496
|
+
"textRange": {"type": "ALL"}, # Apply to all text in the shape
|
497
|
+
"bulletPreset": "BULLET_DISC_CIRCLE_SQUARE",
|
498
|
+
}
|
499
|
+
}
|
500
|
+
]
|
501
|
+
|
502
|
+
# Log the bullet formatting request
|
503
|
+
logger.info(f"Sending API request to apply bullet formatting: {json.dumps(bullet_request[0], indent=2)}")
|
504
|
+
|
505
|
+
bullet_response = (
|
506
|
+
self.service.presentations()
|
507
|
+
.batchUpdate(
|
508
|
+
presentationId=presentation_id,
|
509
|
+
body={"requests": bullet_request},
|
510
|
+
)
|
511
|
+
.execute()
|
512
|
+
)
|
513
|
+
|
514
|
+
# Log the response
|
515
|
+
logger.info(f"API response for bullet formatting: {json.dumps(bullet_response, indent=2)}")
|
516
|
+
except Exception as bullet_error:
|
517
|
+
logger.warning(f"Failed to apply bullet formatting: {str(bullet_error)}")
|
518
|
+
# No fallback here - the text is already added, just without bullets
|
519
|
+
|
520
|
+
return {
|
521
|
+
"presentationId": presentation_id,
|
522
|
+
"slideId": slide_id,
|
523
|
+
"elementId": element_id,
|
524
|
+
"operation": "add_bulleted_list",
|
525
|
+
"result": "success",
|
526
|
+
}
|
527
|
+
except Exception as e:
|
528
|
+
return self.handle_api_error("add_bulleted_list", e)
|
529
|
+
|
530
|
+
def create_presentation_from_markdown(self, title: str, markdown_content: str) -> dict[str, Any]:
|
531
|
+
"""
|
532
|
+
Create a Google Slides presentation from Markdown content using markdowndeck.
|
533
|
+
|
534
|
+
Args:
|
535
|
+
title: Title of the presentation
|
536
|
+
markdown_content: Markdown content to convert to slides
|
537
|
+
|
538
|
+
Returns:
|
539
|
+
Created presentation data
|
540
|
+
"""
|
541
|
+
try:
|
542
|
+
logger.info(f"Creating presentation from markdown: '{title}'")
|
543
|
+
|
544
|
+
# Get credentials
|
545
|
+
credentials = gauth.get_credentials()
|
546
|
+
|
547
|
+
# Use markdowndeck to create the presentation
|
548
|
+
result = create_presentation(markdown=markdown_content, title=title, credentials=credentials)
|
549
|
+
|
550
|
+
logger.info(f"Successfully created presentation with ID: {result.get('presentationId')}")
|
551
|
+
|
552
|
+
# The presentation data is already in the expected format from markdowndeck
|
553
|
+
return result
|
554
|
+
|
555
|
+
except Exception as e:
|
556
|
+
logger.exception(f"Error creating presentation from markdown: {str(e)}")
|
557
|
+
return self.handle_api_error("create_presentation_from_markdown", e)
|
558
|
+
|
559
|
+
def get_slides(self, presentation_id: str) -> list[dict[str, Any]]:
|
560
|
+
"""
|
561
|
+
Get all slides from a presentation.
|
562
|
+
|
563
|
+
Args:
|
564
|
+
presentation_id: The ID of the presentation
|
565
|
+
|
566
|
+
Returns:
|
567
|
+
List of slide data dictionaries or error information
|
568
|
+
"""
|
569
|
+
try:
|
570
|
+
# Get the presentation with slide details
|
571
|
+
presentation = self.service.presentations().get(presentationId=presentation_id).execute()
|
572
|
+
|
573
|
+
# Extract slide information
|
574
|
+
slides = []
|
575
|
+
for slide in presentation.get("slides", []):
|
576
|
+
slide_id = slide.get("objectId", "")
|
577
|
+
|
578
|
+
# Extract page elements
|
579
|
+
elements = []
|
580
|
+
for element in slide.get("pageElements", []):
|
581
|
+
element_type = None
|
582
|
+
element_content = None
|
583
|
+
|
584
|
+
# Determine element type and content
|
585
|
+
if "shape" in element and "text" in element["shape"]:
|
586
|
+
element_type = "text"
|
587
|
+
if "textElements" in element["shape"]["text"]:
|
588
|
+
# Extract text content
|
589
|
+
text_parts = []
|
590
|
+
for text_element in element["shape"]["text"]["textElements"]:
|
591
|
+
if "textRun" in text_element:
|
592
|
+
text_parts.append(text_element["textRun"].get("content", ""))
|
593
|
+
element_content = "".join(text_parts)
|
594
|
+
elif "image" in element:
|
595
|
+
element_type = "image"
|
596
|
+
if "contentUrl" in element["image"]:
|
597
|
+
element_content = element["image"]["contentUrl"]
|
598
|
+
elif "table" in element:
|
599
|
+
element_type = "table"
|
600
|
+
element_content = (
|
601
|
+
f"Table with {element['table'].get('rows', 0)} rows, {element['table'].get('columns', 0)} columns"
|
602
|
+
)
|
603
|
+
|
604
|
+
# Add to elements if we found content
|
605
|
+
if element_type and element_content:
|
606
|
+
elements.append(
|
607
|
+
{
|
608
|
+
"id": element.get("objectId", ""),
|
609
|
+
"type": element_type,
|
610
|
+
"content": element_content,
|
611
|
+
}
|
612
|
+
)
|
613
|
+
|
614
|
+
# Get speaker notes if present
|
615
|
+
notes = ""
|
616
|
+
if "slideProperties" in slide and "notesPage" in slide["slideProperties"]:
|
617
|
+
notes_page = slide["slideProperties"]["notesPage"]
|
618
|
+
if "pageElements" in notes_page:
|
619
|
+
for element in notes_page["pageElements"]:
|
620
|
+
if (
|
621
|
+
"shape" in element
|
622
|
+
and "text" in element["shape"]
|
623
|
+
and "textElements" in element["shape"]["text"]
|
624
|
+
):
|
625
|
+
note_parts = []
|
626
|
+
for text_element in element["shape"]["text"]["textElements"]:
|
627
|
+
if "textRun" in text_element:
|
628
|
+
note_parts.append(text_element["textRun"].get("content", ""))
|
629
|
+
if note_parts:
|
630
|
+
notes = "".join(note_parts)
|
631
|
+
|
632
|
+
# Add slide info to results
|
633
|
+
slides.append(
|
634
|
+
{
|
635
|
+
"id": slide_id,
|
636
|
+
"elements": elements,
|
637
|
+
"notes": notes if notes else None,
|
638
|
+
}
|
639
|
+
)
|
640
|
+
|
641
|
+
return slides
|
642
|
+
except Exception as e:
|
643
|
+
return self.handle_api_error("get_slides", e)
|
644
|
+
|
645
|
+
def delete_slide(self, presentation_id: str, slide_id: str) -> dict[str, Any]:
|
646
|
+
"""
|
647
|
+
Delete a slide from a presentation.
|
648
|
+
|
649
|
+
Args:
|
650
|
+
presentation_id: The ID of the presentation
|
651
|
+
slide_id: The ID of the slide to delete
|
652
|
+
|
653
|
+
Returns:
|
654
|
+
Response data or error information
|
655
|
+
"""
|
656
|
+
try:
|
657
|
+
# Define the delete request
|
658
|
+
requests = [{"deleteObject": {"objectId": slide_id}}]
|
659
|
+
|
660
|
+
logger.info(f"Sending API request to delete slide: {json.dumps(requests[0], indent=2)}")
|
661
|
+
|
662
|
+
# Execute the request
|
663
|
+
response = (
|
664
|
+
self.service.presentations().batchUpdate(presentationId=presentation_id, body={"requests": requests}).execute()
|
665
|
+
)
|
666
|
+
|
667
|
+
logger.info(f"API response for slide deletion: {json.dumps(response, indent=2)}")
|
668
|
+
|
669
|
+
return {
|
670
|
+
"presentationId": presentation_id,
|
671
|
+
"slideId": slide_id,
|
672
|
+
"operation": "delete_slide",
|
673
|
+
"result": "success",
|
674
|
+
}
|
675
|
+
except Exception as e:
|
676
|
+
return self.handle_api_error("delete_slide", e)
|
677
|
+
|
678
|
+
def add_image(
|
679
|
+
self,
|
680
|
+
presentation_id: str,
|
681
|
+
slide_id: str,
|
682
|
+
image_url: str,
|
683
|
+
position: tuple[float, float] = (100, 100),
|
684
|
+
size: tuple[float, float] | None = None,
|
685
|
+
) -> dict[str, Any]:
|
686
|
+
"""
|
687
|
+
Add an image to a slide from a URL.
|
688
|
+
|
689
|
+
Args:
|
690
|
+
presentation_id: The ID of the presentation
|
691
|
+
slide_id: The ID of the slide
|
692
|
+
image_url: The URL of the image to add
|
693
|
+
position: Tuple of (x, y) coordinates for position
|
694
|
+
size: Optional tuple of (width, height) for the image
|
695
|
+
|
696
|
+
Returns:
|
697
|
+
Response data or error information
|
698
|
+
"""
|
699
|
+
try:
|
700
|
+
# Create a unique element ID
|
701
|
+
f"image_{slide_id}_{hash(image_url) % 10000}"
|
702
|
+
|
703
|
+
# Define the base request
|
704
|
+
create_image_request = {
|
705
|
+
"createImage": {
|
706
|
+
"url": image_url,
|
707
|
+
"elementProperties": {
|
708
|
+
"pageObjectId": slide_id,
|
709
|
+
"transform": {
|
710
|
+
"scaleX": 1,
|
711
|
+
"scaleY": 1,
|
712
|
+
"translateX": position[0],
|
713
|
+
"translateY": position[1],
|
714
|
+
"unit": "PT",
|
715
|
+
},
|
716
|
+
},
|
717
|
+
}
|
718
|
+
}
|
719
|
+
|
720
|
+
# Add size if specified
|
721
|
+
if size:
|
722
|
+
create_image_request["createImage"]["elementProperties"]["size"] = {
|
723
|
+
"width": {"magnitude": size[0], "unit": "PT"},
|
724
|
+
"height": {"magnitude": size[1], "unit": "PT"},
|
725
|
+
}
|
726
|
+
|
727
|
+
logger.info(f"Sending API request to create image: {json.dumps(create_image_request, indent=2)}")
|
728
|
+
|
729
|
+
# Execute the request
|
730
|
+
response = (
|
731
|
+
self.service.presentations()
|
732
|
+
.batchUpdate(
|
733
|
+
presentationId=presentation_id,
|
734
|
+
body={"requests": [create_image_request]},
|
735
|
+
)
|
736
|
+
.execute()
|
737
|
+
)
|
738
|
+
|
739
|
+
# Extract the image ID from the response
|
740
|
+
if "replies" in response and len(response["replies"]) > 0:
|
741
|
+
image_id = response["replies"][0].get("createImage", {}).get("objectId")
|
742
|
+
logger.info(f"API response for image creation: {json.dumps(response, indent=2)}")
|
743
|
+
return {
|
744
|
+
"presentationId": presentation_id,
|
745
|
+
"slideId": slide_id,
|
746
|
+
"imageId": image_id,
|
747
|
+
"operation": "add_image",
|
748
|
+
"result": "success",
|
749
|
+
}
|
750
|
+
|
751
|
+
return response
|
752
|
+
except Exception as e:
|
753
|
+
return self.handle_api_error("add_image", e)
|
754
|
+
|
755
|
+
def add_table(
|
756
|
+
self,
|
757
|
+
presentation_id: str,
|
758
|
+
slide_id: str,
|
759
|
+
rows: int,
|
760
|
+
columns: int,
|
761
|
+
data: list[list[str]],
|
762
|
+
position: tuple[float, float] = (100, 100),
|
763
|
+
size: tuple[float, float] = (400, 200),
|
764
|
+
) -> dict[str, Any]:
|
765
|
+
"""
|
766
|
+
Add a table to a slide.
|
767
|
+
|
768
|
+
Args:
|
769
|
+
presentation_id: The ID of the presentation
|
770
|
+
slide_id: The ID of the slide
|
771
|
+
rows: Number of rows in the table
|
772
|
+
columns: Number of columns in the table
|
773
|
+
data: 2D array of strings containing table data
|
774
|
+
position: Tuple of (x, y) coordinates for position
|
775
|
+
size: Tuple of (width, height) for the table
|
776
|
+
|
777
|
+
Returns:
|
778
|
+
Response data or error information
|
779
|
+
"""
|
780
|
+
try:
|
781
|
+
# Create a unique table ID
|
782
|
+
table_id = f"table_{slide_id}_{hash(str(data)) % 10000}"
|
783
|
+
|
784
|
+
# Create table request
|
785
|
+
create_table_request = {
|
786
|
+
"createTable": {
|
787
|
+
"objectId": table_id,
|
788
|
+
"elementProperties": {
|
789
|
+
"pageObjectId": slide_id,
|
790
|
+
"size": {
|
791
|
+
"width": {"magnitude": size[0], "unit": "PT"},
|
792
|
+
"height": {"magnitude": size[1], "unit": "PT"},
|
793
|
+
},
|
794
|
+
"transform": {
|
795
|
+
"scaleX": 1,
|
796
|
+
"scaleY": 1,
|
797
|
+
"translateX": position[0],
|
798
|
+
"translateY": position[1],
|
799
|
+
"unit": "PT",
|
800
|
+
},
|
801
|
+
},
|
802
|
+
"rows": rows,
|
803
|
+
"columns": columns,
|
804
|
+
}
|
805
|
+
}
|
806
|
+
|
807
|
+
logger.info(f"Sending API request to create table: {json.dumps(create_table_request, indent=2)}")
|
808
|
+
|
809
|
+
# Execute table creation
|
810
|
+
response = (
|
811
|
+
self.service.presentations()
|
812
|
+
.batchUpdate(
|
813
|
+
presentationId=presentation_id,
|
814
|
+
body={"requests": [create_table_request]},
|
815
|
+
)
|
816
|
+
.execute()
|
817
|
+
)
|
818
|
+
|
819
|
+
logger.info(f"API response for table creation: {json.dumps(response, indent=2)}")
|
820
|
+
|
821
|
+
# Populate the table if data is provided
|
822
|
+
if data:
|
823
|
+
text_requests = []
|
824
|
+
|
825
|
+
for r, row in enumerate(data):
|
826
|
+
for c, cell_text in enumerate(row):
|
827
|
+
if cell_text and r < rows and c < columns:
|
828
|
+
# Insert text into cell
|
829
|
+
text_requests.append(
|
830
|
+
{
|
831
|
+
"insertText": {
|
832
|
+
"objectId": table_id,
|
833
|
+
"cellLocation": {
|
834
|
+
"rowIndex": r,
|
835
|
+
"columnIndex": c,
|
836
|
+
},
|
837
|
+
"text": cell_text,
|
838
|
+
"insertionIndex": 0,
|
839
|
+
}
|
840
|
+
}
|
841
|
+
)
|
842
|
+
|
843
|
+
if text_requests:
|
844
|
+
logger.info(f"Sending API request to populate table with {len(text_requests)} cell entries")
|
845
|
+
table_text_response = (
|
846
|
+
self.service.presentations()
|
847
|
+
.batchUpdate(
|
848
|
+
presentationId=presentation_id,
|
849
|
+
body={"requests": text_requests},
|
850
|
+
)
|
851
|
+
.execute()
|
852
|
+
)
|
853
|
+
logger.info(f"API response for table population: {json.dumps(table_text_response, indent=2)}")
|
854
|
+
|
855
|
+
return {
|
856
|
+
"presentationId": presentation_id,
|
857
|
+
"slideId": slide_id,
|
858
|
+
"tableId": table_id,
|
859
|
+
"operation": "add_table",
|
860
|
+
"result": "success",
|
861
|
+
}
|
862
|
+
except Exception as e:
|
863
|
+
return self.handle_api_error("add_table", e)
|
864
|
+
|
865
|
+
def add_slide_notes(
|
866
|
+
self,
|
867
|
+
presentation_id: str,
|
868
|
+
slide_id: str,
|
869
|
+
notes_text: str,
|
870
|
+
) -> dict[str, Any]:
|
871
|
+
"""
|
872
|
+
Add presenter notes to a slide.
|
873
|
+
|
874
|
+
Args:
|
875
|
+
presentation_id: The ID of the presentation
|
876
|
+
slide_id: The ID of the slide
|
877
|
+
notes_text: The text content for presenter notes
|
878
|
+
|
879
|
+
Returns:
|
880
|
+
Response data or error information
|
881
|
+
"""
|
882
|
+
try:
|
883
|
+
# Create the update speaker notes request
|
884
|
+
requests = [
|
885
|
+
{
|
886
|
+
"updateSpeakerNotesProperties": {
|
887
|
+
"objectId": slide_id,
|
888
|
+
"speakerNotesProperties": {"speakerNotesText": notes_text},
|
889
|
+
"fields": "speakerNotesText",
|
890
|
+
}
|
891
|
+
}
|
892
|
+
]
|
893
|
+
|
894
|
+
logger.info(f"Sending API request to add slide notes: {json.dumps(requests[0], indent=2)}")
|
895
|
+
|
896
|
+
# Execute the request
|
897
|
+
response = (
|
898
|
+
self.service.presentations().batchUpdate(presentationId=presentation_id, body={"requests": requests}).execute()
|
899
|
+
)
|
900
|
+
|
901
|
+
logger.info(f"API response for slide notes: {json.dumps(response, indent=2)}")
|
902
|
+
|
903
|
+
return {
|
904
|
+
"presentationId": presentation_id,
|
905
|
+
"slideId": slide_id,
|
906
|
+
"operation": "add_slide_notes",
|
907
|
+
"result": "success",
|
908
|
+
}
|
909
|
+
except Exception as e:
|
910
|
+
return self.handle_api_error("add_slide_notes", e)
|
911
|
+
|
912
|
+
def duplicate_slide(self, presentation_id: str, slide_id: str, insert_at_index: int | None = None) -> dict[str, Any]:
|
913
|
+
"""
|
914
|
+
Duplicate a slide in a presentation.
|
915
|
+
|
916
|
+
Args:
|
917
|
+
presentation_id: The ID of the presentation
|
918
|
+
slide_id: The ID of the slide to duplicate
|
919
|
+
insert_at_index: Optional index where to insert the duplicated slide
|
920
|
+
|
921
|
+
Returns:
|
922
|
+
Response data with the new slide ID or error information
|
923
|
+
"""
|
924
|
+
try:
|
925
|
+
# Create the duplicate slide request
|
926
|
+
duplicate_request = {"duplicateObject": {"objectId": slide_id}}
|
927
|
+
|
928
|
+
# If insert location is specified
|
929
|
+
if insert_at_index is not None:
|
930
|
+
duplicate_request["duplicateObject"]["insertionIndex"] = insert_at_index
|
931
|
+
|
932
|
+
logger.info(f"Sending API request to duplicate slide: {json.dumps(duplicate_request, indent=2)}")
|
933
|
+
|
934
|
+
# Execute the duplicate request
|
935
|
+
response = (
|
936
|
+
self.service.presentations()
|
937
|
+
.batchUpdate(
|
938
|
+
presentationId=presentation_id,
|
939
|
+
body={"requests": [duplicate_request]},
|
940
|
+
)
|
941
|
+
.execute()
|
942
|
+
)
|
943
|
+
|
944
|
+
logger.info(f"API response for slide duplication: {json.dumps(response, indent=2)}")
|
945
|
+
|
946
|
+
# Extract the duplicated slide ID
|
947
|
+
new_slide_id = None
|
948
|
+
if "replies" in response and len(response["replies"]) > 0:
|
949
|
+
new_slide_id = response["replies"][0].get("duplicateObject", {}).get("objectId")
|
950
|
+
|
951
|
+
return {
|
952
|
+
"presentationId": presentation_id,
|
953
|
+
"originalSlideId": slide_id,
|
954
|
+
"newSlideId": new_slide_id,
|
955
|
+
"operation": "duplicate_slide",
|
956
|
+
"result": "success",
|
957
|
+
}
|
958
|
+
except Exception as e:
|
959
|
+
return self.handle_api_error("duplicate_slide", e)
|