Spaces:
Running
Running
| from __future__ import annotations | |
| import time | |
| from typing import Any, Dict, Optional | |
| from fastapi import APIRouter, Depends, Header, HTTPException | |
| from app.config import get_settings | |
| from app.core.logger import get_logger | |
| from app.models.schemas import ( | |
| SheetsAppendRequest, | |
| SheetsBatchClearByDataFilterRequest, | |
| SheetsBatchClearRequest, | |
| SheetsBatchGetByDataFilterRequest, | |
| SheetsBatchUpdateByDataFilterRequest, | |
| SheetsBatchUpdateRequest, | |
| SheetsBatchUpdateValuesRequest, | |
| SheetsCopySheetRequest, | |
| SheetsCreateSpreadsheetRequest, | |
| SheetsGenericResponse, | |
| SheetsGetByDataFilterRequest, | |
| SheetsRefreshResponse, | |
| SheetsSearchDeveloperMetadataRequest, | |
| SheetsValueUpdateRequest, | |
| ) | |
| from app.services.sheets_service import ( | |
| SheetsAPIError, | |
| SheetsCredentials, | |
| SheetsService, | |
| ) | |
| router = APIRouter(prefix="/google/sheets", tags=["Google Sheets"]) | |
| _logger = get_logger(__name__) | |
| _settings = get_settings() | |
| _sheets_service = SheetsService() | |
| def get_sheets_service() -> SheetsService: | |
| return _sheets_service | |
| async def close_sheets_service() -> None: | |
| await _sheets_service.close() | |
| def _credentials( | |
| x_access_token: Optional[str] = Header(None, alias="X-Access-Token"), | |
| x_refresh_token: Optional[str] = Header(None, alias="X-Refresh-Token"), | |
| x_client_id: Optional[str] = Header(None, alias="X-Client-Id"), | |
| x_client_secret: Optional[str] = Header(None, alias="X-Client-Secret"), | |
| x_token_expires_at: Optional[float] = Header(None, alias="X-Token-Expires-At"), | |
| ) -> SheetsCredentials: | |
| if not x_access_token: | |
| raise HTTPException( | |
| status_code=401, | |
| detail="X-Access-Token header is required.", | |
| ) | |
| return SheetsCredentials( | |
| access_token=x_access_token, | |
| refresh_token=x_refresh_token, | |
| client_id=x_client_id, | |
| client_secret=x_client_secret, | |
| expires_at=x_token_expires_at, | |
| ) | |
| def _http_error(exc: SheetsAPIError) -> HTTPException: | |
| return HTTPException(status_code=exc.status_code, detail=exc.message) | |
| def _elapsed_ms(start: float) -> float: | |
| return round((time.perf_counter() - start) * 1000, 2) | |
| def _ok( | |
| start: float, | |
| creds: SheetsCredentials, | |
| data: Any = None, | |
| *, | |
| error: Optional[str] = None, | |
| ) -> SheetsGenericResponse: | |
| return SheetsGenericResponse( | |
| success=error is None, | |
| time_ms=_elapsed_ms(start), | |
| data=data, | |
| refreshed_access_token=creds.refreshed_access_token, | |
| error=error, | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Service metadata & token lifecycle | |
| # --------------------------------------------------------------------------- | |
| async def list_scopes( | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| scopes = await service.list_available_scopes() | |
| _logger.info("Listed %d Sheets scopes (%.2fms)", len(scopes), _elapsed_ms(start)) | |
| return SheetsGenericResponse(success=True, time_ms=_elapsed_ms(start), data=scopes) | |
| async def token_refresh( | |
| body: Dict[str, str], | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| client_id = body.get("client_id", "") | |
| client_secret = body.get("client_secret", "") | |
| refresh_token = body.get("refresh_token", "") | |
| if not (client_id and client_secret and refresh_token): | |
| raise HTTPException( | |
| status_code=400, | |
| detail="client_id, client_secret and refresh_token are required.", | |
| ) | |
| try: | |
| tokens = await service.refresh_access_token( | |
| client_id=client_id, | |
| client_secret=client_secret, | |
| refresh_token=refresh_token, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets token refreshed (%.2fms)", _elapsed_ms(start)) | |
| return SheetsRefreshResponse( | |
| success=True, | |
| access_token=tokens.get("access_token"), | |
| expires_in=int(tokens.get("expires_in", 0)), | |
| token_type=tokens.get("token_type", "Bearer"), | |
| scope=tokens.get("scope"), | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Spreadsheets | |
| # --------------------------------------------------------------------------- | |
| async def create_spreadsheet( | |
| body: SheetsCreateSpreadsheetRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.create_spreadsheet( | |
| creds, | |
| title=body.title, | |
| sheets=body.sheets, | |
| properties=body.properties, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets spreadsheet '%s' created (%.2fms)", body.title, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def list_spreadsheets( | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| page_size: int = 100, | |
| page_token: Optional[str] = None, | |
| include_trashed: bool = False, | |
| ): | |
| start = time.perf_counter() | |
| if page_size < 1 or page_size > 1000: | |
| raise HTTPException(status_code=400, detail="page_size must be between 1 and 1000.") | |
| try: | |
| data = await service.list_spreadsheets( | |
| creds, | |
| page_size=page_size, | |
| page_token=page_token, | |
| include_trashed=include_trashed, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| files = (data or {}).get("files", []) or [] | |
| _logger.info("Sheets listed %d spreadsheets (%.2fms)", len(files), _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def get_spreadsheet( | |
| spreadsheet_id: str, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ranges: Optional[str] = None, | |
| include_grid_data: bool = False, | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.get_spreadsheet( | |
| creds, | |
| spreadsheet_id, | |
| ranges=ranges.split(",") if ranges else None, | |
| include_grid_data=include_grid_data, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets spreadsheet %s fetched (%.2fms)", spreadsheet_id, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def delete_spreadsheet( | |
| spreadsheet_id: str, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.delete_spreadsheet(creds, spreadsheet_id) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets spreadsheet %s deleted (%.2fms)", spreadsheet_id, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def get_sheet( | |
| spreadsheet_id: str, | |
| sheet_id: int, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ranges: Optional[str] = None, | |
| include_grid_data: bool = False, | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.get_sheet( | |
| creds, | |
| spreadsheet_id, | |
| sheet_id, | |
| ranges=ranges.split(",") if ranges else None, | |
| include_grid_data=include_grid_data, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets sheet %d of %s fetched (%.2fms)", sheet_id, spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |
| async def batch_update( | |
| spreadsheet_id: str, | |
| body: SheetsBatchUpdateRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.batch_update( | |
| creds, spreadsheet_id, requests=body.requests | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets spreadsheet %s batch-updated (%.2fms)", spreadsheet_id, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def get_spreadsheet_by_data_filter( | |
| spreadsheet_id: str, | |
| body: SheetsGetByDataFilterRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.get_spreadsheet_by_data_filter( | |
| creds, | |
| spreadsheet_id, | |
| data_filters=body.data_filters, | |
| include_grid_data=body.include_grid_data, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets spreadsheet %s read by data filter (%.2fms)", spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |
| async def copy_sheet( | |
| spreadsheet_id: str, | |
| sheet_id: int, | |
| body: SheetsCopySheetRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.copy_sheet( | |
| creds, | |
| spreadsheet_id, | |
| sheet_id, | |
| destination_spreadsheet_id=body.destination_spreadsheet_id, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets sheet %d copied from %s (%.2fms)", sheet_id, spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |
| # --------------------------------------------------------------------------- | |
| # Values | |
| # --------------------------------------------------------------------------- | |
| async def get_values( | |
| spreadsheet_id: str, | |
| range: str, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| major_dimension: Optional[str] = None, | |
| value_render_option: Optional[str] = None, | |
| date_time_render_option: Optional[str] = None, | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.get_values( | |
| creds, | |
| spreadsheet_id, | |
| range, | |
| major_dimension=major_dimension, | |
| value_render_option=value_render_option, | |
| date_time_render_option=date_time_render_option, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets values %s!%s fetched (%.2fms)", spreadsheet_id, range, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def batch_get_values( | |
| spreadsheet_id: str, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ranges: Optional[str] = None, | |
| major_dimension: Optional[str] = None, | |
| value_render_option: Optional[str] = None, | |
| date_time_render_option: Optional[str] = None, | |
| ): | |
| start = time.perf_counter() | |
| if not ranges: | |
| raise HTTPException(status_code=400, detail="At least one 'ranges' value is required.") | |
| try: | |
| data = await service.batch_get_values( | |
| creds, | |
| spreadsheet_id, | |
| ranges=ranges.split(","), | |
| major_dimension=major_dimension, | |
| value_render_option=value_render_option, | |
| date_time_render_option=date_time_render_option, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets batchGet %s fetched (%.2fms)", spreadsheet_id, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def update_values( | |
| spreadsheet_id: str, | |
| range: str, | |
| body: SheetsValueUpdateRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| value_input_option: str = "RAW", | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.update_values( | |
| creds, | |
| spreadsheet_id, | |
| range, | |
| values=body.values, | |
| major_dimension=body.major_dimension, | |
| value_input_option=value_input_option, | |
| include_values_in_response=body.include_values_in_response, | |
| value_render_option=body.value_render_option, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets values %s!%s updated (%.2fms)", spreadsheet_id, range, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def append_values( | |
| spreadsheet_id: str, | |
| range: str, | |
| body: SheetsAppendRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| value_input_option: str = "USER_ENTERED", | |
| insert_data_option: Optional[str] = None, | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.append_values( | |
| creds, | |
| spreadsheet_id, | |
| range, | |
| values=body.values, | |
| major_dimension=body.major_dimension, | |
| value_input_option=value_input_option, | |
| insert_data_option=insert_data_option, | |
| include_values_in_response=False, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets values %s!%s appended (%.2fms)", spreadsheet_id, range, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def clear_values( | |
| spreadsheet_id: str, | |
| range: str, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.clear_values(creds, spreadsheet_id, range) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets values %s!%s cleared (%.2fms)", spreadsheet_id, range, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def batch_update_values( | |
| spreadsheet_id: str, | |
| body: SheetsBatchUpdateValuesRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| value_input_option: str = "RAW", | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.batch_update_values( | |
| creds, | |
| spreadsheet_id, | |
| data=body.data, | |
| value_input_option=value_input_option, | |
| include_values_in_response=body.include_values_in_response, | |
| value_render_option=body.value_render_option, | |
| response_value_render_option=body.response_value_render_option, | |
| response_date_time_render_option=body.response_date_time_render_option, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets values batch-updated on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def batch_clear_values( | |
| spreadsheet_id: str, | |
| body: SheetsBatchClearRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.batch_clear_values( | |
| creds, spreadsheet_id, ranges=body.ranges | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info("Sheets values batch-cleared on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start)) | |
| return _ok(start, creds, data) | |
| async def batch_get_values_by_data_filter( | |
| spreadsheet_id: str, | |
| body: SheetsBatchGetByDataFilterRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.batch_get_values_by_data_filter( | |
| creds, | |
| spreadsheet_id, | |
| data_filters=body.data_filters, | |
| major_dimension=body.major_dimension, | |
| value_render_option=body.value_render_option, | |
| date_time_render_option=body.date_time_render_option, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets batchGetByDataFilter on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |
| async def batch_update_values_by_data_filter( | |
| spreadsheet_id: str, | |
| body: SheetsBatchUpdateByDataFilterRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| value_input_option: str = "RAW", | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.batch_update_values_by_data_filter( | |
| creds, | |
| spreadsheet_id, | |
| data=body.data, | |
| value_input_option=value_input_option, | |
| include_values_in_response=body.include_values_in_response, | |
| value_render_option=body.value_render_option, | |
| response_value_render_option=body.response_value_render_option, | |
| response_date_time_render_option=body.response_date_time_render_option, | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets batchUpdateByDataFilter on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |
| async def batch_clear_values_by_data_filter( | |
| spreadsheet_id: str, | |
| body: SheetsBatchClearByDataFilterRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.batch_clear_values_by_data_filter( | |
| creds, spreadsheet_id, data_filters=body.data_filters | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets batchClearByDataFilter on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |
| # --------------------------------------------------------------------------- | |
| # Developer metadata | |
| # --------------------------------------------------------------------------- | |
| async def get_developer_metadata( | |
| spreadsheet_id: str, | |
| metadata_id: int, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.get_developer_metadata(creds, spreadsheet_id, metadata_id) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets developer metadata %d fetched from %s (%.2fms)", | |
| metadata_id, | |
| spreadsheet_id, | |
| _elapsed_ms(start), | |
| ) | |
| return _ok(start, creds, data) | |
| async def search_developer_metadata( | |
| spreadsheet_id: str, | |
| body: SheetsSearchDeveloperMetadataRequest, | |
| creds: SheetsCredentials = Depends(_credentials), | |
| service: SheetsService = Depends(get_sheets_service), | |
| ): | |
| start = time.perf_counter() | |
| try: | |
| data = await service.search_developer_metadata( | |
| creds, spreadsheet_id, data_filters=body.data_filters or None | |
| ) | |
| except SheetsAPIError as exc: | |
| raise _http_error(exc) from exc | |
| _logger.info( | |
| "Sheets developer metadata searched on %s (%.2fms)", spreadsheet_id, _elapsed_ms(start) | |
| ) | |
| return _ok(start, creds, data) | |