google-workspace-mcp 1.1.5__py3-none-any.whl → 1.2.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.
@@ -36,7 +36,9 @@ class SheetsService(BaseGoogleService):
36
36
  try:
37
37
  logger.info(f"Creating new Google Spreadsheet with title: '{title}'")
38
38
  spreadsheet_body = {"properties": {"title": title}}
39
- spreadsheet = self.service.spreadsheets().create(body=spreadsheet_body).execute()
39
+ spreadsheet = (
40
+ self.service.spreadsheets().create(body=spreadsheet_body).execute()
41
+ )
40
42
  spreadsheet_id = spreadsheet.get("spreadsheetId")
41
43
  logger.info(
42
44
  f"Successfully created spreadsheet: {spreadsheet.get('properties', {}).get('title')} (ID: {spreadsheet_id})"
@@ -71,8 +73,15 @@ class SheetsService(BaseGoogleService):
71
73
  or an error dictionary.
72
74
  """
73
75
  try:
74
- logger.info(f"Reading range '{range_a1}' from spreadsheet ID: {spreadsheet_id}")
75
- result = self.service.spreadsheets().values().get(spreadsheetId=spreadsheet_id, range=range_a1).execute()
76
+ logger.info(
77
+ f"Reading range '{range_a1}' from spreadsheet ID: {spreadsheet_id}"
78
+ )
79
+ result = (
80
+ self.service.spreadsheets()
81
+ .values()
82
+ .get(spreadsheetId=spreadsheet_id, range=range_a1)
83
+ .execute()
84
+ )
76
85
 
77
86
  # result will contain 'range', 'majorDimension', 'values'
78
87
  # 'values' is a list of lists.
@@ -82,15 +91,23 @@ class SheetsService(BaseGoogleService):
82
91
  return {
83
92
  "spreadsheet_id": spreadsheet_id,
84
93
  "range_requested": range_a1, # The input range
85
- "range_returned": result.get("range"), # The actual range returned by API
94
+ "range_returned": result.get(
95
+ "range"
96
+ ), # The actual range returned by API
86
97
  "major_dimension": result.get("majorDimension"),
87
- "values": result.get("values", []), # Default to empty list if no values
98
+ "values": result.get(
99
+ "values", []
100
+ ), # Default to empty list if no values
88
101
  }
89
102
  except HttpError as error:
90
- logger.error(f"Error reading range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}")
103
+ logger.error(
104
+ f"Error reading range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}"
105
+ )
91
106
  return self.handle_api_error("read_range", error)
92
107
  except Exception as e:
93
- logger.exception(f"Unexpected error reading range '{range_a1}' from spreadsheet {spreadsheet_id}")
108
+ logger.exception(
109
+ f"Unexpected error reading range '{range_a1}' from spreadsheet {spreadsheet_id}"
110
+ )
94
111
  return {
95
112
  "error": True,
96
113
  "error_type": "unexpected_service_error",
@@ -140,17 +157,23 @@ class SheetsService(BaseGoogleService):
140
157
  f"Successfully wrote to range '{result.get('updatedRange')}' in spreadsheet ID: {spreadsheet_id}. Updated {result.get('updatedCells')} cells."
141
158
  )
142
159
  return {
143
- "spreadsheet_id": result.get("spreadsheetId"), # Or use the input spreadsheet_id
160
+ "spreadsheet_id": result.get(
161
+ "spreadsheetId"
162
+ ), # Or use the input spreadsheet_id
144
163
  "updated_range": result.get("updatedRange"),
145
164
  "updated_rows": result.get("updatedRows"),
146
165
  "updated_columns": result.get("updatedColumns"),
147
166
  "updated_cells": result.get("updatedCells"),
148
167
  }
149
168
  except HttpError as error:
150
- logger.error(f"Error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}")
169
+ logger.error(
170
+ f"Error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}"
171
+ )
151
172
  return self.handle_api_error("write_range", error)
152
173
  except Exception as e:
153
- logger.exception(f"Unexpected error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}")
174
+ logger.exception(
175
+ f"Unexpected error writing to range '{range_a1}' in spreadsheet {spreadsheet_id}"
176
+ )
154
177
  return {
155
178
  "error": True,
156
179
  "error_type": "unexpected_service_error",
@@ -200,17 +223,29 @@ class SheetsService(BaseGoogleService):
200
223
  .execute()
201
224
  )
202
225
  # result typically includes: spreadsheetId, tableRange (if appended to a table), updates (if named ranges/etc. were affected)
203
- logger.info(f"Successfully appended rows to spreadsheet ID: {spreadsheet_id}. Updates: {result.get('updates')}")
226
+ logger.info(
227
+ f"Successfully appended rows to spreadsheet ID: {spreadsheet_id}. Updates: {result.get('updates')}"
228
+ )
204
229
  return {
205
- "spreadsheet_id": result.get("spreadsheetId"), # Or use the input spreadsheet_id
206
- "table_range_updated": result.get("tableRange"), # The range of the new data
207
- "updates": result.get("updates"), # Info about other updates, e.g. to named ranges
230
+ "spreadsheet_id": result.get(
231
+ "spreadsheetId"
232
+ ), # Or use the input spreadsheet_id
233
+ "table_range_updated": result.get(
234
+ "tableRange"
235
+ ), # The range of the new data
236
+ "updates": result.get(
237
+ "updates"
238
+ ), # Info about other updates, e.g. to named ranges
208
239
  }
209
240
  except HttpError as error:
210
- logger.error(f"Error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}")
241
+ logger.error(
242
+ f"Error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}: {error}"
243
+ )
211
244
  return self.handle_api_error("append_rows", error)
212
245
  except Exception as e:
213
- logger.exception(f"Unexpected error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}")
246
+ logger.exception(
247
+ f"Unexpected error appending rows to range '{range_a1}' in spreadsheet {spreadsheet_id}"
248
+ )
214
249
  return {
215
250
  "error": True,
216
251
  "error_type": "unexpected_service_error",
@@ -231,22 +266,35 @@ class SheetsService(BaseGoogleService):
231
266
  A dictionary containing the cleared range and spreadsheet ID, or an error dictionary.
232
267
  """
233
268
  try:
234
- logger.info(f"Clearing range '{range_a1}' in spreadsheet ID: {spreadsheet_id}")
269
+ logger.info(
270
+ f"Clearing range '{range_a1}' in spreadsheet ID: {spreadsheet_id}"
271
+ )
235
272
  # The body for clear is an empty JSON object {}.
236
273
  result = (
237
- self.service.spreadsheets().values().clear(spreadsheetId=spreadsheet_id, range=range_a1, body={}).execute()
274
+ self.service.spreadsheets()
275
+ .values()
276
+ .clear(spreadsheetId=spreadsheet_id, range=range_a1, body={})
277
+ .execute()
238
278
  )
239
279
  # result typically includes: spreadsheetId, clearedRange
240
- logger.info(f"Successfully cleared range '{result.get('clearedRange')}' in spreadsheet ID: {spreadsheet_id}.")
280
+ logger.info(
281
+ f"Successfully cleared range '{result.get('clearedRange')}' in spreadsheet ID: {spreadsheet_id}."
282
+ )
241
283
  return {
242
- "spreadsheet_id": result.get("spreadsheetId"), # Or use the input spreadsheet_id
284
+ "spreadsheet_id": result.get(
285
+ "spreadsheetId"
286
+ ), # Or use the input spreadsheet_id
243
287
  "cleared_range": result.get("clearedRange"),
244
288
  }
245
289
  except HttpError as error:
246
- logger.error(f"Error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}")
290
+ logger.error(
291
+ f"Error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}: {error}"
292
+ )
247
293
  return self.handle_api_error("clear_range", error)
248
294
  except Exception as e:
249
- logger.exception(f"Unexpected error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}")
295
+ logger.exception(
296
+ f"Unexpected error clearing range '{range_a1}' from spreadsheet {spreadsheet_id}"
297
+ )
250
298
  return {
251
299
  "error": True,
252
300
  "error_type": "unexpected_service_error",
@@ -267,10 +315,16 @@ class SheetsService(BaseGoogleService):
267
315
  or an error dictionary.
268
316
  """
269
317
  try:
270
- logger.info(f"Adding new sheet with title '{title}' to spreadsheet ID: {spreadsheet_id}")
318
+ logger.info(
319
+ f"Adding new sheet with title '{title}' to spreadsheet ID: {spreadsheet_id}"
320
+ )
271
321
  requests = [{"addSheet": {"properties": {"title": title}}}]
272
322
  body = {"requests": requests}
273
- response = self.service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
323
+ response = (
324
+ self.service.spreadsheets()
325
+ .batchUpdate(spreadsheetId=spreadsheet_id, body=body)
326
+ .execute()
327
+ )
274
328
 
275
329
  # The response contains a list of replies, one for each request.
276
330
  # The addSheet reply contains the properties of the new sheet.
@@ -300,10 +354,14 @@ class SheetsService(BaseGoogleService):
300
354
  "sheet_properties": new_sheet_properties,
301
355
  }
302
356
  except HttpError as error:
303
- logger.error(f"Error adding sheet '{title}' to spreadsheet {spreadsheet_id}: {error}")
357
+ logger.error(
358
+ f"Error adding sheet '{title}' to spreadsheet {spreadsheet_id}: {error}"
359
+ )
304
360
  return self.handle_api_error("add_sheet", error)
305
361
  except Exception as e:
306
- logger.exception(f"Unexpected error adding sheet '{title}' to spreadsheet {spreadsheet_id}")
362
+ logger.exception(
363
+ f"Unexpected error adding sheet '{title}' to spreadsheet {spreadsheet_id}"
364
+ )
307
365
  return {
308
366
  "error": True,
309
367
  "error_type": "unexpected_service_error",
@@ -323,10 +381,16 @@ class SheetsService(BaseGoogleService):
323
381
  A dictionary indicating success (spreadsheetId and deleted sheetId) or an error dictionary.
324
382
  """
325
383
  try:
326
- logger.info(f"Deleting sheet ID: {sheet_id} from spreadsheet ID: {spreadsheet_id}")
384
+ logger.info(
385
+ f"Deleting sheet ID: {sheet_id} from spreadsheet ID: {spreadsheet_id}"
386
+ )
327
387
  requests = [{"deleteSheet": {"sheetId": sheet_id}}]
328
388
  body = {"requests": requests}
329
- response = self.service.spreadsheets().batchUpdate(spreadsheetId=spreadsheet_id, body=body).execute()
389
+ response = (
390
+ self.service.spreadsheets()
391
+ .batchUpdate(spreadsheetId=spreadsheet_id, body=body)
392
+ .execute()
393
+ )
330
394
 
331
395
  # A successful deleteSheet request usually doesn't return detailed content in the reply.
332
396
  # The overall response.spreadsheetId confirms the operation was on the correct spreadsheet.
@@ -339,10 +403,14 @@ class SheetsService(BaseGoogleService):
339
403
  "success": True,
340
404
  }
341
405
  except HttpError as error:
342
- logger.error(f"Error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}: {error}")
406
+ logger.error(
407
+ f"Error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}: {error}"
408
+ )
343
409
  return self.handle_api_error("delete_sheet", error)
344
410
  except Exception as e:
345
- logger.exception(f"Unexpected error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}")
411
+ logger.exception(
412
+ f"Unexpected error deleting sheet ID {sheet_id} from spreadsheet {spreadsheet_id}"
413
+ )
346
414
  return {
347
415
  "error": True,
348
416
  "error_type": "unexpected_service_error",
@@ -350,7 +418,9 @@ class SheetsService(BaseGoogleService):
350
418
  "operation": "delete_sheet",
351
419
  }
352
420
 
353
- def get_spreadsheet_metadata(self, spreadsheet_id: str, fields: str | None = None) -> dict[str, Any] | None:
421
+ def get_spreadsheet_metadata(
422
+ self, spreadsheet_id: str, fields: str | None = None
423
+ ) -> dict[str, Any] | None:
354
424
  """
355
425
  Retrieves metadata for a specific Google Spreadsheet.
356
426
 
@@ -364,24 +434,192 @@ class SheetsService(BaseGoogleService):
364
434
  """
365
435
  try:
366
436
  logger.info(
367
- f"Fetching metadata for spreadsheet ID: {spreadsheet_id}" + (f" with fields: {fields}" if fields else "")
437
+ f"Fetching metadata for spreadsheet ID: {spreadsheet_id}"
438
+ + (f" with fields: {fields}" if fields else "")
368
439
  )
369
440
  if fields is None:
370
441
  fields = "spreadsheetId,properties,sheets(properties(sheetId,title,index,sheetType,gridProperties))"
371
442
 
372
- spreadsheet_metadata = self.service.spreadsheets().get(spreadsheetId=spreadsheet_id, fields=fields).execute()
443
+ spreadsheet_metadata = (
444
+ self.service.spreadsheets()
445
+ .get(spreadsheetId=spreadsheet_id, fields=fields)
446
+ .execute()
447
+ )
373
448
  logger.info(
374
449
  f"Successfully fetched metadata for spreadsheet: {spreadsheet_metadata.get('properties', {}).get('title')}"
375
450
  )
376
451
  return spreadsheet_metadata
377
452
  except HttpError as error:
378
- logger.error(f"Error fetching metadata for spreadsheet ID {spreadsheet_id}: {error}")
453
+ logger.error(
454
+ f"Error fetching metadata for spreadsheet ID {spreadsheet_id}: {error}"
455
+ )
379
456
  return self.handle_api_error("get_spreadsheet_metadata", error)
380
457
  except Exception as e:
381
- logger.exception(f"Unexpected error fetching metadata for spreadsheet ID {spreadsheet_id}")
458
+ logger.exception(
459
+ f"Unexpected error fetching metadata for spreadsheet ID {spreadsheet_id}"
460
+ )
382
461
  return {
383
462
  "error": True,
384
463
  "error_type": "unexpected_service_error",
385
464
  "message": str(e),
386
465
  "operation": "get_spreadsheet_metadata",
387
466
  }
467
+
468
+ def create_chart_on_sheet(
469
+ self,
470
+ spreadsheet_id: str,
471
+ sheet_id: int,
472
+ chart_type: str,
473
+ num_rows: int,
474
+ num_cols: int,
475
+ title: str,
476
+ ) -> dict[str, Any] | None:
477
+ """
478
+ Adds a new chart to a sheet, handling different chart types correctly.
479
+
480
+ Args:
481
+ spreadsheet_id: The ID of the Google Spreadsheet.
482
+ sheet_id: The numeric ID of the sheet to add the chart to.
483
+ chart_type: The API-specific type of chart ('COLUMN', 'LINE', 'PIE_CHART').
484
+ num_rows: The number of rows in the data range.
485
+ num_cols: The number of columns in the data range.
486
+ title: The title of the chart.
487
+
488
+ Returns:
489
+ A dictionary containing the properties of the newly created chart or an error dictionary.
490
+ """
491
+ try:
492
+ logger.info(f"Constructing spec for chart '{title}' of type '{chart_type}'")
493
+
494
+ chart_spec = {"title": title}
495
+
496
+ if chart_type == "PIE_CHART":
497
+ chart_spec["pieChart"] = {
498
+ "domain": {
499
+ "sourceRange": {
500
+ "sources": [
501
+ {
502
+ "sheetId": sheet_id,
503
+ "startRowIndex": 0,
504
+ "endRowIndex": num_rows,
505
+ "startColumnIndex": 0,
506
+ "endColumnIndex": 1,
507
+ }
508
+ ]
509
+ }
510
+ },
511
+ "series": {
512
+ "sourceRange": {
513
+ "sources": [
514
+ {
515
+ "sheetId": sheet_id,
516
+ "startRowIndex": 0,
517
+ "endRowIndex": num_rows,
518
+ "startColumnIndex": 1,
519
+ "endColumnIndex": 2,
520
+ }
521
+ ]
522
+ }
523
+ },
524
+ "legendPosition": "LABELED_LEGEND",
525
+ }
526
+ else: # For BAR, COLUMN, LINE charts
527
+ # --- START OF FIX: Correctly structure the domain object ---
528
+ domain_spec = {
529
+ "domain": {
530
+ "sourceRange": {
531
+ "sources": [
532
+ {
533
+ "sheetId": sheet_id,
534
+ "startRowIndex": 0,
535
+ "endRowIndex": num_rows,
536
+ "startColumnIndex": 0,
537
+ "endColumnIndex": 1,
538
+ }
539
+ ]
540
+ }
541
+ }
542
+ }
543
+ # --- END OF FIX ---
544
+
545
+ series = []
546
+ for i in range(1, num_cols):
547
+ series.append(
548
+ {
549
+ "series": {
550
+ "sourceRange": {
551
+ "sources": [
552
+ {
553
+ "sheetId": sheet_id,
554
+ "startRowIndex": 0,
555
+ "endRowIndex": num_rows,
556
+ "startColumnIndex": i,
557
+ "endColumnIndex": i + 1,
558
+ }
559
+ ]
560
+ }
561
+ },
562
+ "targetAxis": "LEFT_AXIS",
563
+ }
564
+ )
565
+
566
+ chart_spec["basicChart"] = {
567
+ "chartType": chart_type,
568
+ "legendPosition": "BOTTOM_LEGEND",
569
+ "axis": [
570
+ {"position": "BOTTOM_AXIS"},
571
+ {"position": "LEFT_AXIS"},
572
+ ],
573
+ "domains": [domain_spec], # Use the corrected domain object
574
+ "series": series,
575
+ }
576
+
577
+ requests = [
578
+ {
579
+ "addChart": {
580
+ "chart": {
581
+ "spec": chart_spec,
582
+ "position": {
583
+ "overlayPosition": {
584
+ "anchorCell": {
585
+ "sheetId": sheet_id,
586
+ "rowIndex": num_rows + 2,
587
+ "columnIndex": 0,
588
+ },
589
+ }
590
+ },
591
+ }
592
+ }
593
+ }
594
+ ]
595
+
596
+ body = {"requests": requests}
597
+ # The google-api-python-client expects snake_case keys in the body,
598
+ # so we'll convert our camelCase spec to snake_case before sending.
599
+ # This is a good practice for robustness.
600
+ import humps
601
+
602
+ snake_body = humps.decamelize(body)
603
+
604
+ response = (
605
+ self.service.spreadsheets()
606
+ .batchUpdate(spreadsheetId=spreadsheet_id, body=snake_body)
607
+ .execute()
608
+ )
609
+
610
+ new_chart_properties = (
611
+ response.get("replies", [{}])[0].get("addChart", {}).get("chart")
612
+ )
613
+ if not new_chart_properties:
614
+ raise ValueError("Failed to create chart or parse response.")
615
+
616
+ logger.info(
617
+ f"Successfully created chart with ID: {new_chart_properties.get('chartId')}"
618
+ )
619
+ return new_chart_properties
620
+
621
+ except HttpError as error:
622
+ logger.error(f"Google API error in create_chart_on_sheet: {error.content}")
623
+ return self.handle_api_error("create_chart_on_sheet", error)
624
+ except Exception as e:
625
+ return self.handle_api_error("create_chart_on_sheet", e)