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.
Files changed (38) hide show
  1. google_workspace_mcp/__init__.py +3 -0
  2. google_workspace_mcp/__main__.py +43 -0
  3. google_workspace_mcp/app.py +8 -0
  4. google_workspace_mcp/auth/__init__.py +7 -0
  5. google_workspace_mcp/auth/gauth.py +62 -0
  6. google_workspace_mcp/config.py +60 -0
  7. google_workspace_mcp/prompts/__init__.py +3 -0
  8. google_workspace_mcp/prompts/calendar.py +36 -0
  9. google_workspace_mcp/prompts/drive.py +18 -0
  10. google_workspace_mcp/prompts/gmail.py +65 -0
  11. google_workspace_mcp/prompts/slides.py +40 -0
  12. google_workspace_mcp/resources/__init__.py +13 -0
  13. google_workspace_mcp/resources/calendar.py +79 -0
  14. google_workspace_mcp/resources/drive.py +93 -0
  15. google_workspace_mcp/resources/gmail.py +58 -0
  16. google_workspace_mcp/resources/sheets_resources.py +92 -0
  17. google_workspace_mcp/resources/slides.py +421 -0
  18. google_workspace_mcp/services/__init__.py +21 -0
  19. google_workspace_mcp/services/base.py +73 -0
  20. google_workspace_mcp/services/calendar.py +256 -0
  21. google_workspace_mcp/services/docs_service.py +388 -0
  22. google_workspace_mcp/services/drive.py +454 -0
  23. google_workspace_mcp/services/gmail.py +676 -0
  24. google_workspace_mcp/services/sheets_service.py +466 -0
  25. google_workspace_mcp/services/slides.py +959 -0
  26. google_workspace_mcp/tools/__init__.py +7 -0
  27. google_workspace_mcp/tools/calendar.py +229 -0
  28. google_workspace_mcp/tools/docs_tools.py +277 -0
  29. google_workspace_mcp/tools/drive.py +221 -0
  30. google_workspace_mcp/tools/gmail.py +344 -0
  31. google_workspace_mcp/tools/sheets_tools.py +322 -0
  32. google_workspace_mcp/tools/slides.py +478 -0
  33. google_workspace_mcp/utils/__init__.py +1 -0
  34. google_workspace_mcp/utils/markdown_slides.py +504 -0
  35. google_workspace_mcp-1.0.0.dist-info/METADATA +547 -0
  36. google_workspace_mcp-1.0.0.dist-info/RECORD +38 -0
  37. google_workspace_mcp-1.0.0.dist-info/WHEEL +4 -0
  38. 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)