validops-east-1 commited on
Commit
3d1f304
·
1 Parent(s): 8ec8b4a

feat: complete gmail and sheets api method coverage

Browse files
app/api/v1/gmail.py CHANGED
@@ -26,6 +26,7 @@ from app.models.schemas import (
26
  GmailRefreshResponse,
27
  GmailSendResponse,
28
  GmailSettingsUpdateRequest,
 
29
  GmailWatchRequest,
30
  )
31
  from app.services.gmail_service import (
@@ -1356,6 +1357,98 @@ async def verify_send_as(
1356
  return _ok(start, creds, {"verification_sent": send_as_email})
1357
 
1358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1359
  @router.get("/settings/delegates", response_model=GmailGenericResponse,
1360
  summary="List delegates")
1361
  async def list_delegates(
 
26
  GmailRefreshResponse,
27
  GmailSendResponse,
28
  GmailSettingsUpdateRequest,
29
+ GmailSmimeInfoRequest,
30
  GmailWatchRequest,
31
  )
32
  from app.services.gmail_service import (
 
1357
  return _ok(start, creds, {"verification_sent": send_as_email})
1358
 
1359
 
1360
+ @router.get("/settings/send-as/{send_as_email}/smime-info", response_model=GmailGenericResponse,
1361
+ summary="List S/MIME certificates for a send-as alias (smimeInfo.list)")
1362
+ async def list_smime_info(
1363
+ send_as_email: str,
1364
+ creds: GmailCredentials = Depends(_credentials),
1365
+ service: GmailService = Depends(get_gmail_service),
1366
+ user_id: Optional[str] = None,
1367
+ ):
1368
+ start = time.perf_counter()
1369
+ try:
1370
+ data = await service.list_smime_info(creds, send_as_email, user_id=user_id or "me")
1371
+ except GmailAPIError as exc:
1372
+ raise _http_error(exc) from exc
1373
+ _logger.info("Gmail smimeInfo listed for %s (%.2fms)", send_as_email, _elapsed_ms(start))
1374
+ return _ok(start, creds, data)
1375
+
1376
+
1377
+ @router.post("/settings/send-as/{send_as_email}/smime-info", response_model=GmailGenericResponse,
1378
+ summary="Insert an S/MIME certificate for a send-as alias (smimeInfo.insert)")
1379
+ async def insert_smime_info(
1380
+ send_as_email: str,
1381
+ body: GmailSmimeInfoRequest,
1382
+ creds: GmailCredentials = Depends(_credentials),
1383
+ service: GmailService = Depends(get_gmail_service),
1384
+ user_id: Optional[str] = None,
1385
+ ):
1386
+ start = time.perf_counter()
1387
+ try:
1388
+ data = await service.insert_smime_info(creds, send_as_email, body.payload, user_id=user_id or "me")
1389
+ except GmailAPIError as exc:
1390
+ raise _http_error(exc) from exc
1391
+ _logger.info("Gmail smimeInfo inserted for %s (%.2fms)", send_as_email, _elapsed_ms(start))
1392
+ return _ok(start, creds, data)
1393
+
1394
+
1395
+ @router.get("/settings/send-as/{send_as_email}/smime-info/{smime_info_id}",
1396
+ response_model=GmailGenericResponse,
1397
+ summary="Get a single S/MIME certificate (smimeInfo.get)")
1398
+ async def get_smime_info(
1399
+ send_as_email: str,
1400
+ smime_info_id: str,
1401
+ creds: GmailCredentials = Depends(_credentials),
1402
+ service: GmailService = Depends(get_gmail_service),
1403
+ user_id: Optional[str] = None,
1404
+ ):
1405
+ start = time.perf_counter()
1406
+ try:
1407
+ data = await service.get_smime_info(creds, send_as_email, smime_info_id, user_id=user_id or "me")
1408
+ except GmailAPIError as exc:
1409
+ raise _http_error(exc) from exc
1410
+ _logger.info("Gmail smimeInfo %s fetched (%.2fms)", smime_info_id, _elapsed_ms(start))
1411
+ return _ok(start, creds, data)
1412
+
1413
+
1414
+ @router.delete("/settings/send-as/{send_as_email}/smime-info/{smime_info_id}",
1415
+ response_model=GmailGenericResponse,
1416
+ summary="Delete an S/MIME certificate (smimeInfo.delete)")
1417
+ async def delete_smime_info(
1418
+ send_as_email: str,
1419
+ smime_info_id: str,
1420
+ creds: GmailCredentials = Depends(_credentials),
1421
+ service: GmailService = Depends(get_gmail_service),
1422
+ user_id: Optional[str] = None,
1423
+ ):
1424
+ start = time.perf_counter()
1425
+ try:
1426
+ await service.delete_smime_info(creds, send_as_email, smime_info_id, user_id=user_id or "me")
1427
+ except GmailAPIError as exc:
1428
+ raise _http_error(exc) from exc
1429
+ _logger.info("Gmail smimeInfo %s deleted (%.2fms)", smime_info_id, _elapsed_ms(start))
1430
+ return _ok(start, creds, {"deleted": smime_info_id})
1431
+
1432
+
1433
+ @router.post("/settings/send-as/{send_as_email}/smime-info/{smime_info_id}/setDefault",
1434
+ response_model=GmailGenericResponse,
1435
+ summary="Set an S/MIME certificate as the default for a send-as alias (smimeInfo.setDefault)")
1436
+ async def set_default_smime_info(
1437
+ send_as_email: str,
1438
+ smime_info_id: str,
1439
+ creds: GmailCredentials = Depends(_credentials),
1440
+ service: GmailService = Depends(get_gmail_service),
1441
+ user_id: Optional[str] = None,
1442
+ ):
1443
+ start = time.perf_counter()
1444
+ try:
1445
+ await service.set_default_smime_info(creds, send_as_email, smime_info_id, user_id=user_id or "me")
1446
+ except GmailAPIError as exc:
1447
+ raise _http_error(exc) from exc
1448
+ _logger.info("Gmail smimeInfo %s set default (%.2fms)", smime_info_id, _elapsed_ms(start))
1449
+ return _ok(start, creds, {"default_set": smime_info_id})
1450
+
1451
+
1452
  @router.get("/settings/delegates", response_model=GmailGenericResponse,
1453
  summary="List delegates")
1454
  async def list_delegates(
app/api/v1/sheets.py CHANGED
@@ -9,10 +9,18 @@ from app.config import get_settings
9
  from app.core.logger import get_logger
10
  from app.models.schemas import (
11
  SheetsAppendRequest,
 
 
 
 
12
  SheetsBatchUpdateRequest,
 
 
13
  SheetsCreateSpreadsheetRequest,
14
  SheetsGenericResponse,
 
15
  SheetsRefreshResponse,
 
16
  SheetsValueUpdateRequest,
17
  )
18
  from app.services.sheets_service import (
@@ -225,6 +233,57 @@ async def batch_update(
225
  return _ok(start, creds, data)
226
 
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  # ---------------------------------------------------------------------------
229
  # Values
230
  # ---------------------------------------------------------------------------
@@ -362,3 +421,179 @@ async def clear_values(
362
  raise _http_error(exc) from exc
363
  _logger.info("Sheets values %s!%s cleared (%.2fms)", spreadsheet_id, range, _elapsed_ms(start))
364
  return _ok(start, creds, data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from app.core.logger import get_logger
10
  from app.models.schemas import (
11
  SheetsAppendRequest,
12
+ SheetsBatchClearByDataFilterRequest,
13
+ SheetsBatchClearRequest,
14
+ SheetsBatchGetByDataFilterRequest,
15
+ SheetsBatchUpdateByDataFilterRequest,
16
  SheetsBatchUpdateRequest,
17
+ SheetsBatchUpdateValuesRequest,
18
+ SheetsCopySheetRequest,
19
  SheetsCreateSpreadsheetRequest,
20
  SheetsGenericResponse,
21
+ SheetsGetByDataFilterRequest,
22
  SheetsRefreshResponse,
23
+ SheetsSearchDeveloperMetadataRequest,
24
  SheetsValueUpdateRequest,
25
  )
26
  from app.services.sheets_service import (
 
233
  return _ok(start, creds, data)
234
 
235
 
236
+ @router.post("/spreadsheets/{spreadsheet_id}:getByDataFilter",
237
+ response_model=SheetsGenericResponse,
238
+ summary="Read a spreadsheet filtered by DataFilter objects (spreadsheets.getByDataFilter)")
239
+ async def get_spreadsheet_by_data_filter(
240
+ spreadsheet_id: str,
241
+ body: SheetsGetByDataFilterRequest,
242
+ creds: SheetsCredentials = Depends(_credentials),
243
+ service: SheetsService = Depends(get_sheets_service),
244
+ ):
245
+ start = time.perf_counter()
246
+ try:
247
+ data = await service.get_spreadsheet_by_data_filter(
248
+ creds,
249
+ spreadsheet_id,
250
+ data_filters=body.data_filters,
251
+ include_grid_data=body.include_grid_data,
252
+ )
253
+ except SheetsAPIError as exc:
254
+ raise _http_error(exc) from exc
255
+ _logger.info(
256
+ "Sheets spreadsheet %s read by data filter (%.2fms)", spreadsheet_id, _elapsed_ms(start)
257
+ )
258
+ return _ok(start, creds, data)
259
+
260
+
261
+ @router.post("/spreadsheets/{spreadsheet_id}/sheets/{sheet_id}:copyTo",
262
+ response_model=SheetsGenericResponse,
263
+ summary="Copy a sheet to another spreadsheet (sheets.copyTo)")
264
+ async def copy_sheet(
265
+ spreadsheet_id: str,
266
+ sheet_id: int,
267
+ body: SheetsCopySheetRequest,
268
+ creds: SheetsCredentials = Depends(_credentials),
269
+ service: SheetsService = Depends(get_sheets_service),
270
+ ):
271
+ start = time.perf_counter()
272
+ try:
273
+ data = await service.copy_sheet(
274
+ creds,
275
+ spreadsheet_id,
276
+ sheet_id,
277
+ destination_spreadsheet_id=body.destination_spreadsheet_id,
278
+ )
279
+ except SheetsAPIError as exc:
280
+ raise _http_error(exc) from exc
281
+ _logger.info(
282
+ "Sheets sheet %d copied from %s (%.2fms)", sheet_id, spreadsheet_id, _elapsed_ms(start)
283
+ )
284
+ return _ok(start, creds, data)
285
+
286
+
287
  # ---------------------------------------------------------------------------
288
  # Values
289
  # ---------------------------------------------------------------------------
 
421
  raise _http_error(exc) from exc
422
  _logger.info("Sheets values %s!%s cleared (%.2fms)", spreadsheet_id, range, _elapsed_ms(start))
423
  return _ok(start, creds, data)
424
+
425
+
426
+ @router.post("/spreadsheets/{spreadsheet_id}/values:batchUpdate",
427
+ response_model=SheetsGenericResponse,
428
+ summary="Write multiple ranges in one call (values.batchUpdate)")
429
+ async def batch_update_values(
430
+ spreadsheet_id: str,
431
+ body: SheetsBatchUpdateValuesRequest,
432
+ creds: SheetsCredentials = Depends(_credentials),
433
+ service: SheetsService = Depends(get_sheets_service),
434
+ value_input_option: str = "RAW",
435
+ ):
436
+ start = time.perf_counter()
437
+ try:
438
+ data = await service.batch_update_values(
439
+ creds,
440
+ spreadsheet_id,
441
+ data=body.data,
442
+ value_input_option=value_input_option,
443
+ include_values_in_response=body.include_values_in_response,
444
+ value_render_option=body.value_render_option,
445
+ response_value_render_option=body.response_value_render_option,
446
+ response_date_time_render_option=body.response_date_time_render_option,
447
+ )
448
+ except SheetsAPIError as exc:
449
+ raise _http_error(exc) from exc
450
+ _logger.info("Sheets values batch-updated on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start))
451
+ return _ok(start, creds, data)
452
+
453
+
454
+ @router.post("/spreadsheets/{spreadsheet_id}/values:batchClear",
455
+ response_model=SheetsGenericResponse,
456
+ summary="Clear multiple ranges in one call (values.batchClear)")
457
+ async def batch_clear_values(
458
+ spreadsheet_id: str,
459
+ body: SheetsBatchClearRequest,
460
+ creds: SheetsCredentials = Depends(_credentials),
461
+ service: SheetsService = Depends(get_sheets_service),
462
+ ):
463
+ start = time.perf_counter()
464
+ try:
465
+ data = await service.batch_clear_values(
466
+ creds, spreadsheet_id, ranges=body.ranges
467
+ )
468
+ except SheetsAPIError as exc:
469
+ raise _http_error(exc) from exc
470
+ _logger.info("Sheets values batch-cleared on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start))
471
+ return _ok(start, creds, data)
472
+
473
+
474
+ @router.post("/spreadsheets/{spreadsheet_id}/values:batchGetByDataFilter",
475
+ response_model=SheetsGenericResponse,
476
+ summary="Read multiple ranges by DataFilter (values.batchGetByDataFilter)")
477
+ async def batch_get_values_by_data_filter(
478
+ spreadsheet_id: str,
479
+ body: SheetsBatchGetByDataFilterRequest,
480
+ creds: SheetsCredentials = Depends(_credentials),
481
+ service: SheetsService = Depends(get_sheets_service),
482
+ ):
483
+ start = time.perf_counter()
484
+ try:
485
+ data = await service.batch_get_values_by_data_filter(
486
+ creds,
487
+ spreadsheet_id,
488
+ data_filters=body.data_filters,
489
+ major_dimension=body.major_dimension,
490
+ value_render_option=body.value_render_option,
491
+ date_time_render_option=body.date_time_render_option,
492
+ )
493
+ except SheetsAPIError as exc:
494
+ raise _http_error(exc) from exc
495
+ _logger.info(
496
+ "Sheets batchGetByDataFilter on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start)
497
+ )
498
+ return _ok(start, creds, data)
499
+
500
+
501
+ @router.post("/spreadsheets/{spreadsheet_id}/values:batchUpdateByDataFilter",
502
+ response_model=SheetsGenericResponse,
503
+ summary="Write multiple DataFilter ranges (values.batchUpdateByDataFilter)")
504
+ async def batch_update_values_by_data_filter(
505
+ spreadsheet_id: str,
506
+ body: SheetsBatchUpdateByDataFilterRequest,
507
+ creds: SheetsCredentials = Depends(_credentials),
508
+ service: SheetsService = Depends(get_sheets_service),
509
+ value_input_option: str = "RAW",
510
+ ):
511
+ start = time.perf_counter()
512
+ try:
513
+ data = await service.batch_update_values_by_data_filter(
514
+ creds,
515
+ spreadsheet_id,
516
+ data=body.data,
517
+ value_input_option=value_input_option,
518
+ include_values_in_response=body.include_values_in_response,
519
+ value_render_option=body.value_render_option,
520
+ response_value_render_option=body.response_value_render_option,
521
+ response_date_time_render_option=body.response_date_time_render_option,
522
+ )
523
+ except SheetsAPIError as exc:
524
+ raise _http_error(exc) from exc
525
+ _logger.info(
526
+ "Sheets batchUpdateByDataFilter on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start)
527
+ )
528
+ return _ok(start, creds, data)
529
+
530
+
531
+ @router.post("/spreadsheets/{spreadsheet_id}/values:batchClearByDataFilter",
532
+ response_model=SheetsGenericResponse,
533
+ summary="Clear multiple DataFilter ranges (values.batchClearByDataFilter)")
534
+ async def batch_clear_values_by_data_filter(
535
+ spreadsheet_id: str,
536
+ body: SheetsBatchClearByDataFilterRequest,
537
+ creds: SheetsCredentials = Depends(_credentials),
538
+ service: SheetsService = Depends(get_sheets_service),
539
+ ):
540
+ start = time.perf_counter()
541
+ try:
542
+ data = await service.batch_clear_values_by_data_filter(
543
+ creds, spreadsheet_id, data_filters=body.data_filters
544
+ )
545
+ except SheetsAPIError as exc:
546
+ raise _http_error(exc) from exc
547
+ _logger.info(
548
+ "Sheets batchClearByDataFilter on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start)
549
+ )
550
+ return _ok(start, creds, data)
551
+
552
+
553
+ # ---------------------------------------------------------------------------
554
+ # Developer metadata
555
+ # ---------------------------------------------------------------------------
556
+
557
+ @router.get("/spreadsheets/{spreadsheet_id}/developerMetadata/{metadata_id}",
558
+ response_model=SheetsGenericResponse,
559
+ summary="Get a single developer metadata entry (developerMetadata.get)")
560
+ async def get_developer_metadata(
561
+ spreadsheet_id: str,
562
+ metadata_id: int,
563
+ creds: SheetsCredentials = Depends(_credentials),
564
+ service: SheetsService = Depends(get_sheets_service),
565
+ ):
566
+ start = time.perf_counter()
567
+ try:
568
+ data = await service.get_developer_metadata(creds, spreadsheet_id, metadata_id)
569
+ except SheetsAPIError as exc:
570
+ raise _http_error(exc) from exc
571
+ _logger.info(
572
+ "Sheets developer metadata %d fetched from %s (%.2fms)",
573
+ metadata_id,
574
+ spreadsheet_id,
575
+ _elapsed_ms(start),
576
+ )
577
+ return _ok(start, creds, data)
578
+
579
+
580
+ @router.post("/spreadsheets/{spreadsheet_id}/developerMetadata:search",
581
+ response_model=SheetsGenericResponse,
582
+ summary="Search developer metadata (developerMetadata.search)")
583
+ async def search_developer_metadata(
584
+ spreadsheet_id: str,
585
+ body: SheetsSearchDeveloperMetadataRequest,
586
+ creds: SheetsCredentials = Depends(_credentials),
587
+ service: SheetsService = Depends(get_sheets_service),
588
+ ):
589
+ start = time.perf_counter()
590
+ try:
591
+ data = await service.search_developer_metadata(
592
+ creds, spreadsheet_id, data_filters=body.data_filters or None
593
+ )
594
+ except SheetsAPIError as exc:
595
+ raise _http_error(exc) from exc
596
+ _logger.info(
597
+ "Sheets developer metadata searched on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start)
598
+ )
599
+ return _ok(start, creds, data)
app/models/schemas.py CHANGED
@@ -1964,6 +1964,22 @@ class GmailSendAsRequest(BaseModel):
1964
  signature: Optional[str] = Field(None, description="Email signature (HTML)")
1965
 
1966
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1967
  class GmailGenericResponse(BaseModel):
1968
  """Standard Gmail operation envelope."""
1969
 
@@ -2053,6 +2069,83 @@ class SheetsBatchUpdateRequest(BaseModel):
2053
  requests: List[Dict[str, Any]] = Field(..., min_length=1, description="List of request objects (addSheet, updateCells, etc.)")
2054
 
2055
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2056
  class SheetsGenericResponse(BaseModel):
2057
  """Standard Google Sheets operation envelope."""
2058
 
 
1964
  signature: Optional[str] = Field(None, description="Email signature (HTML)")
1965
 
1966
 
1967
+ class GmailSmimeInfoRequest(BaseModel):
1968
+ """Body for inserting an S/MIME certificate (users.settings.sendAs.smimeInfo.insert)."""
1969
+
1970
+ payload: Dict[str, Any] = Field(
1971
+ ...,
1972
+ description="SmimeInfo resource: either 'pkcs12' (base64) + 'encryptedKeyPassword', or 'pem'. See Gmail API reference.",
1973
+ )
1974
+
1975
+ @field_validator("payload")
1976
+ @classmethod
1977
+ def _non_empty(cls, v: Dict[str, Any]) -> Dict[str, Any]:
1978
+ if not v:
1979
+ raise ValueError("payload must not be empty.")
1980
+ return v
1981
+
1982
+
1983
  class GmailGenericResponse(BaseModel):
1984
  """Standard Gmail operation envelope."""
1985
 
 
2069
  requests: List[Dict[str, Any]] = Field(..., min_length=1, description="List of request objects (addSheet, updateCells, etc.)")
2070
 
2071
 
2072
+ class SheetsGetByDataFilterRequest(BaseModel):
2073
+ """Body for reading a spreadsheet filtered by DataFilter (spreadsheets.getByDataFilter)."""
2074
+
2075
+ data_filters: List[Dict[str, Any]] = Field(
2076
+ ...,
2077
+ min_length=1,
2078
+ description="List of DataFilter objects; each must set exactly one of a1Range, gridRange, namedRangeId, developerMetadataLookup",
2079
+ )
2080
+ include_grid_data: bool = Field(False, description="Whether to include cell grid data in the response")
2081
+
2082
+
2083
+ class SheetsCopySheetRequest(BaseModel):
2084
+ """Body for copying a sheet to another spreadsheet (sheets.copyTo)."""
2085
+
2086
+ destination_spreadsheet_id: str = Field(
2087
+ ..., min_length=1, description="ID of the spreadsheet to copy the sheet into"
2088
+ )
2089
+
2090
+
2091
+ class SheetsBatchGetByDataFilterRequest(BaseModel):
2092
+ """Body for reading multiple ranges by DataFilter (values.batchGetByDataFilter)."""
2093
+
2094
+ data_filters: List[Dict[str, Any]] = Field(..., min_length=1, description="List of DataFilter objects")
2095
+ major_dimension: Optional[str] = Field(None, pattern="^(ROWS|COLUMNS)$", description="Major dimension of the returned values")
2096
+ value_render_option: Optional[str] = Field(None, pattern="^(FORMATTED_VALUE|UNFORMATTED_VALUE|FORMULA)$", description="Render option for returned values")
2097
+ date_time_render_option: Optional[str] = Field(None, pattern="^(SERIAL_NUMBER|FORMATTED_STRING)$", description="Date/time render option for returned values")
2098
+
2099
+
2100
+ class SheetsBatchUpdateValuesRequest(BaseModel):
2101
+ """Body for writing multiple ranges in one call (values.batchUpdate)."""
2102
+
2103
+ data: List[Dict[str, Any]] = Field(
2104
+ ..., min_length=1, description="List of ValueRange objects, each with 'range' and 'values'"
2105
+ )
2106
+ include_values_in_response: Optional[bool] = Field(None, description="Whether responses should include updated values")
2107
+ value_render_option: Optional[str] = Field(None, pattern="^(FORMATTED_VALUE|UNFORMATTED_VALUE|FORMULA)$", description="Render option for values in the response")
2108
+ response_value_render_option: Optional[str] = Field(None, pattern="^(FORMATTED_VALUE|UNFORMATTED_VALUE|FORMULA)$", description="Render option for response values")
2109
+ response_date_time_render_option: Optional[str] = Field(None, pattern="^(SERIAL_NUMBER|FORMATTED_STRING)$", description="Date/time render option for response values")
2110
+
2111
+
2112
+ class SheetsBatchUpdateByDataFilterRequest(BaseModel):
2113
+ """Body for writing multiple DataFilter ranges (values.batchUpdateByDataFilter)."""
2114
+
2115
+ data: List[Dict[str, Any]] = Field(
2116
+ ...,
2117
+ min_length=1,
2118
+ description="List of DataFilterValueRange objects, each with 'dataFilter', optional 'majorDimension' and 'values'",
2119
+ )
2120
+ include_values_in_response: Optional[bool] = Field(None, description="Whether responses should include updated values")
2121
+ value_render_option: Optional[str] = Field(None, pattern="^(FORMATTED_VALUE|UNFORMATTED_VALUE|FORMULA)$", description="Render option for values in the response")
2122
+ response_value_render_option: Optional[str] = Field(None, pattern="^(FORMATTED_VALUE|UNFORMATTED_VALUE|FORMULA)$", description="Render option for response values")
2123
+ response_date_time_render_option: Optional[str] = Field(None, pattern="^(SERIAL_NUMBER|FORMATTED_STRING)$", description="Date/time render option for response values")
2124
+
2125
+
2126
+ class SheetsBatchClearRequest(BaseModel):
2127
+ """Body for clearing multiple ranges in one call (values.batchClear)."""
2128
+
2129
+ ranges: List[str] = Field(
2130
+ ..., min_length=1, description="A1 notation ranges to clear, e.g. ['Sheet1!A1:B2']"
2131
+ )
2132
+
2133
+
2134
+ class SheetsBatchClearByDataFilterRequest(BaseModel):
2135
+ """Body for clearing multiple DataFilter ranges (values.batchClearByDataFilter)."""
2136
+
2137
+ data_filters: List[Dict[str, Any]] = Field(..., min_length=1, description="List of DataFilter objects")
2138
+
2139
+
2140
+ class SheetsSearchDeveloperMetadataRequest(BaseModel):
2141
+ """Body for searching developer metadata (developerMetadata.search)."""
2142
+
2143
+ data_filters: List[Dict[str, Any]] = Field(
2144
+ default_factory=list,
2145
+ description="Optional list of DataFilter objects, each with developerMetadataLookup; empty searches all metadata",
2146
+ )
2147
+
2148
+
2149
  class SheetsGenericResponse(BaseModel):
2150
  """Standard Google Sheets operation envelope."""
2151
 
app/services/gmail_service.py CHANGED
@@ -1024,3 +1024,63 @@ class GmailService:
1024
  await self._request(
1025
  creds, "DELETE", f"/users/{user_id}/settings/delegates/{delegate_email}"
1026
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1024
  await self._request(
1025
  creds, "DELETE", f"/users/{user_id}/settings/delegates/{delegate_email}"
1026
  )
1027
+
1028
+ async def list_smime_info(
1029
+ self, creds: GmailCredentials, send_as_email: str, user_id: str = "me"
1030
+ ) -> Dict[str, Any]:
1031
+ return await self._request(
1032
+ creds, "GET", f"/users/{user_id}/settings/sendAs/{send_as_email}/smimeInfo"
1033
+ )
1034
+
1035
+ async def get_smime_info(
1036
+ self,
1037
+ creds: GmailCredentials,
1038
+ send_as_email: str,
1039
+ smime_info_id: str,
1040
+ user_id: str = "me",
1041
+ ) -> Dict[str, Any]:
1042
+ return await self._request(
1043
+ creds,
1044
+ "GET",
1045
+ f"/users/{user_id}/settings/sendAs/{send_as_email}/smimeInfo/{smime_info_id}",
1046
+ )
1047
+
1048
+ async def insert_smime_info(
1049
+ self,
1050
+ creds: GmailCredentials,
1051
+ send_as_email: str,
1052
+ payload: Dict[str, Any],
1053
+ user_id: str = "me",
1054
+ ) -> Dict[str, Any]:
1055
+ return await self._request(
1056
+ creds,
1057
+ "POST",
1058
+ f"/users/{user_id}/settings/sendAs/{send_as_email}/smimeInfo",
1059
+ json_body=payload,
1060
+ )
1061
+
1062
+ async def delete_smime_info(
1063
+ self,
1064
+ creds: GmailCredentials,
1065
+ send_as_email: str,
1066
+ smime_info_id: str,
1067
+ user_id: str = "me",
1068
+ ) -> None:
1069
+ await self._request(
1070
+ creds,
1071
+ "DELETE",
1072
+ f"/users/{user_id}/settings/sendAs/{send_as_email}/smimeInfo/{smime_info_id}",
1073
+ )
1074
+
1075
+ async def set_default_smime_info(
1076
+ self,
1077
+ creds: GmailCredentials,
1078
+ send_as_email: str,
1079
+ smime_info_id: str,
1080
+ user_id: str = "me",
1081
+ ) -> None:
1082
+ await self._request(
1083
+ creds,
1084
+ "POST",
1085
+ f"/users/{user_id}/settings/sendAs/{send_as_email}/smimeInfo/{smime_info_id}/setDefault",
1086
+ )
app/services/sheets_service.py CHANGED
@@ -561,3 +561,175 @@ class SheetsService:
561
  f"/spreadsheets/{spreadsheet_id}/values/{range_}:clear",
562
  json_body={},
563
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  f"/spreadsheets/{spreadsheet_id}/values/{range_}:clear",
562
  json_body={},
563
  )
564
+
565
+ async def batch_update_values(
566
+ self,
567
+ creds: SheetsCredentials,
568
+ spreadsheet_id: str,
569
+ *,
570
+ data: List[Dict[str, Any]],
571
+ value_input_option: str = "RAW",
572
+ include_values_in_response: Optional[bool] = None,
573
+ value_render_option: Optional[str] = None,
574
+ response_value_render_option: Optional[str] = None,
575
+ response_date_time_render_option: Optional[str] = None,
576
+ ) -> Dict[str, Any]:
577
+ body: Dict[str, Any] = {"data": data}
578
+ if include_values_in_response is not None:
579
+ body["includeValuesInResponse"] = include_values_in_response
580
+ if value_render_option:
581
+ body["valueRenderOption"] = value_render_option
582
+ if response_value_render_option:
583
+ body["responseValueRenderOption"] = response_value_render_option
584
+ if response_date_time_render_option:
585
+ body["responseDateTimeRenderOption"] = response_date_time_render_option
586
+ return await self._request(
587
+ creds,
588
+ "POST",
589
+ f"/spreadsheets/{spreadsheet_id}/values:batchUpdate",
590
+ params={"valueInputOption": value_input_option},
591
+ json_body=body,
592
+ )
593
+
594
+ async def batch_clear_values(
595
+ self,
596
+ creds: SheetsCredentials,
597
+ spreadsheet_id: str,
598
+ *,
599
+ ranges: List[str],
600
+ ) -> Dict[str, Any]:
601
+ return await self._request(
602
+ creds,
603
+ "POST",
604
+ f"/spreadsheets/{spreadsheet_id}/values:batchClear",
605
+ json_body={"ranges": ranges},
606
+ )
607
+
608
+ async def get_spreadsheet_by_data_filter(
609
+ self,
610
+ creds: SheetsCredentials,
611
+ spreadsheet_id: str,
612
+ *,
613
+ data_filters: List[Dict[str, Any]],
614
+ include_grid_data: bool = False,
615
+ ) -> Dict[str, Any]:
616
+ body: Dict[str, Any] = {"dataFilters": data_filters}
617
+ if include_grid_data:
618
+ body["includeGridData"] = True
619
+ return await self._request(
620
+ creds,
621
+ "POST",
622
+ f"/spreadsheets/{spreadsheet_id}:getByDataFilter",
623
+ json_body=body,
624
+ )
625
+
626
+ async def copy_sheet(
627
+ self,
628
+ creds: SheetsCredentials,
629
+ spreadsheet_id: str,
630
+ sheet_id: int,
631
+ *,
632
+ destination_spreadsheet_id: str,
633
+ ) -> Dict[str, Any]:
634
+ return await self._request(
635
+ creds,
636
+ "POST",
637
+ f"/spreadsheets/{spreadsheet_id}/sheets/{sheet_id}:copyTo",
638
+ json_body={"destinationSpreadsheetId": destination_spreadsheet_id},
639
+ )
640
+
641
+ async def batch_get_values_by_data_filter(
642
+ self,
643
+ creds: SheetsCredentials,
644
+ spreadsheet_id: str,
645
+ *,
646
+ data_filters: List[Dict[str, Any]],
647
+ major_dimension: Optional[str] = None,
648
+ value_render_option: Optional[str] = None,
649
+ date_time_render_option: Optional[str] = None,
650
+ ) -> Dict[str, Any]:
651
+ body: Dict[str, Any] = {"dataFilters": data_filters}
652
+ if major_dimension:
653
+ body["majorDimension"] = major_dimension
654
+ if value_render_option:
655
+ body["valueRenderOption"] = value_render_option
656
+ if date_time_render_option:
657
+ body["dateTimeRenderOption"] = date_time_render_option
658
+ return await self._request(
659
+ creds,
660
+ "POST",
661
+ f"/spreadsheets/{spreadsheet_id}/values:batchGetByDataFilter",
662
+ json_body=body,
663
+ )
664
+
665
+ async def batch_update_values_by_data_filter(
666
+ self,
667
+ creds: SheetsCredentials,
668
+ spreadsheet_id: str,
669
+ *,
670
+ data: List[Dict[str, Any]],
671
+ value_input_option: str = "RAW",
672
+ include_values_in_response: Optional[bool] = None,
673
+ value_render_option: Optional[str] = None,
674
+ response_value_render_option: Optional[str] = None,
675
+ response_date_time_render_option: Optional[str] = None,
676
+ ) -> Dict[str, Any]:
677
+ body: Dict[str, Any] = {"data": data}
678
+ if include_values_in_response is not None:
679
+ body["includeValuesInResponse"] = include_values_in_response
680
+ if value_render_option:
681
+ body["valueRenderOption"] = value_render_option
682
+ if response_value_render_option:
683
+ body["responseValueRenderOption"] = response_value_render_option
684
+ if response_date_time_render_option:
685
+ body["responseDateTimeRenderOption"] = response_date_time_render_option
686
+ return await self._request(
687
+ creds,
688
+ "POST",
689
+ f"/spreadsheets/{spreadsheet_id}/values:batchUpdateByDataFilter",
690
+ params={"valueInputOption": value_input_option},
691
+ json_body=body,
692
+ )
693
+
694
+ async def batch_clear_values_by_data_filter(
695
+ self,
696
+ creds: SheetsCredentials,
697
+ spreadsheet_id: str,
698
+ *,
699
+ data_filters: List[Dict[str, Any]],
700
+ ) -> Dict[str, Any]:
701
+ return await self._request(
702
+ creds,
703
+ "POST",
704
+ f"/spreadsheets/{spreadsheet_id}/values:batchClearByDataFilter",
705
+ json_body={"dataFilters": data_filters},
706
+ )
707
+
708
+ async def get_developer_metadata(
709
+ self,
710
+ creds: SheetsCredentials,
711
+ spreadsheet_id: str,
712
+ metadata_id: int,
713
+ ) -> Dict[str, Any]:
714
+ return await self._request(
715
+ creds,
716
+ "GET",
717
+ f"/spreadsheets/{spreadsheet_id}/developerMetadata/{metadata_id}",
718
+ )
719
+
720
+ async def search_developer_metadata(
721
+ self,
722
+ creds: SheetsCredentials,
723
+ spreadsheet_id: str,
724
+ *,
725
+ data_filters: Optional[List[Dict[str, Any]]] = None,
726
+ ) -> Dict[str, Any]:
727
+ body: Dict[str, Any] = {}
728
+ if data_filters:
729
+ body["dataFilters"] = data_filters
730
+ return await self._request(
731
+ creds,
732
+ "POST",
733
+ f"/spreadsheets/{spreadsheet_id}/developerMetadata:search",
734
+ json_body=body,
735
+ )