Skip to main content
Glama
table_operation_manager.py12.5 kB
""" Table Operation Manager This module provides high-level table operations that orchestrate multiple Google Docs API calls for complex table manipulations. """ import logging import asyncio from typing import List, Dict, Any, Tuple from gdocs.docs_helpers import create_insert_table_request from gdocs.docs_structure import find_tables from gdocs.docs_tables import validate_table_data logger = logging.getLogger(__name__) class TableOperationManager: """ High-level manager for Google Docs table operations. Handles complex multi-step table operations including: - Creating tables with data population - Populating existing tables - Managing cell-by-cell operations with proper index refreshing """ def __init__(self, service): """ Initialize the table operation manager. Args: service: Google Docs API service instance """ self.service = service async def create_and_populate_table( self, document_id: str, table_data: List[List[str]], index: int, bold_headers: bool = True ) -> Tuple[bool, str, Dict[str, Any]]: """ Creates a table and populates it with data in a reliable multi-step process. This method extracts the complex logic from create_table_with_data tool function. Args: document_id: ID of the document to update table_data: 2D list of strings for table content index: Position to insert the table bold_headers: Whether to make the first row bold Returns: Tuple of (success, message, metadata) """ logger.debug(f"Creating table at index {index}, dimensions: {len(table_data)}x{len(table_data[0]) if table_data and len(table_data) > 0 else 0}") # Validate input data is_valid, error_msg = validate_table_data(table_data) if not is_valid: return False, f"Invalid table data: {error_msg}", {} rows = len(table_data) cols = len(table_data[0]) try: # Step 1: Create empty table await self._create_empty_table(document_id, index, rows, cols) # Step 2: Get fresh document structure to find actual cell positions fresh_tables = await self._get_document_tables(document_id) if not fresh_tables: return False, "Could not find table after creation", {} # Step 3: Populate each cell with proper index refreshing population_count = await self._populate_table_cells( document_id, table_data, bold_headers ) metadata = { 'rows': rows, 'columns': cols, 'populated_cells': population_count, 'table_index': len(fresh_tables) - 1 } return True, f"Successfully created {rows}x{cols} table and populated {population_count} cells", metadata except Exception as e: logger.error(f"Failed to create and populate table: {str(e)}") return False, f"Table creation failed: {str(e)}", {} async def _create_empty_table( self, document_id: str, index: int, rows: int, cols: int ) -> None: """Create an empty table at the specified index.""" logger.debug(f"Creating {rows}x{cols} table at index {index}") await asyncio.to_thread( self.service.documents().batchUpdate( documentId=document_id, body={'requests': [create_insert_table_request(index, rows, cols)]} ).execute ) async def _get_document_tables(self, document_id: str) -> List[Dict[str, Any]]: """Get fresh document structure and extract table information.""" doc = await asyncio.to_thread( self.service.documents().get(documentId=document_id).execute ) return find_tables(doc) async def _populate_table_cells( self, document_id: str, table_data: List[List[str]], bold_headers: bool ) -> int: """ Populate table cells with data, refreshing structure after each insertion. This prevents index shifting issues by getting fresh cell positions before each insertion. """ population_count = 0 for row_idx, row_data in enumerate(table_data): logger.debug(f"Processing row {row_idx}: {len(row_data)} cells") for col_idx, cell_text in enumerate(row_data): if not cell_text: # Skip empty cells continue try: # CRITICAL: Refresh document structure before each insertion success = await self._populate_single_cell( document_id, row_idx, col_idx, cell_text, bold_headers and row_idx == 0 ) if success: population_count += 1 logger.debug(f"Populated cell ({row_idx},{col_idx})") else: logger.warning(f"Failed to populate cell ({row_idx},{col_idx})") except Exception as e: logger.error(f"Error populating cell ({row_idx},{col_idx}): {str(e)}") return population_count async def _populate_single_cell( self, document_id: str, row_idx: int, col_idx: int, cell_text: str, apply_bold: bool = False ) -> bool: """ Populate a single cell with text, with optional bold formatting. Returns True if successful, False otherwise. """ try: # Get fresh table structure to avoid index shifting issues tables = await self._get_document_tables(document_id) if not tables: return False table = tables[-1] # Use the last table (newly created one) cells = table.get('cells', []) # Bounds checking if row_idx >= len(cells) or col_idx >= len(cells[row_idx]): logger.error(f"Cell ({row_idx},{col_idx}) out of bounds") return False cell = cells[row_idx][col_idx] insertion_index = cell.get('insertion_index') if not insertion_index: logger.warning(f"No insertion_index for cell ({row_idx},{col_idx})") return False # Insert text await asyncio.to_thread( self.service.documents().batchUpdate( documentId=document_id, body={'requests': [{ 'insertText': { 'location': {'index': insertion_index}, 'text': cell_text } }]} ).execute ) # Apply bold formatting if requested if apply_bold: await self._apply_bold_formatting( document_id, insertion_index, insertion_index + len(cell_text) ) return True except Exception as e: logger.error(f"Failed to populate single cell: {str(e)}") return False async def _apply_bold_formatting( self, document_id: str, start_index: int, end_index: int ) -> None: """Apply bold formatting to a text range.""" await asyncio.to_thread( self.service.documents().batchUpdate( documentId=document_id, body={'requests': [{ 'updateTextStyle': { 'range': { 'startIndex': start_index, 'endIndex': end_index }, 'textStyle': {'bold': True}, 'fields': 'bold' } }]} ).execute ) async def populate_existing_table( self, document_id: str, table_index: int, table_data: List[List[str]], clear_existing: bool = False ) -> Tuple[bool, str, Dict[str, Any]]: """ Populate an existing table with data. Args: document_id: ID of the document table_index: Index of the table to populate (0-based) table_data: 2D list of data to insert clear_existing: Whether to clear existing content first Returns: Tuple of (success, message, metadata) """ try: tables = await self._get_document_tables(document_id) if table_index >= len(tables): return False, f"Table index {table_index} not found. Document has {len(tables)} tables", {} table_info = tables[table_index] # Validate dimensions table_rows = table_info['rows'] table_cols = table_info['columns'] data_rows = len(table_data) data_cols = len(table_data[0]) if table_data else 0 if data_rows > table_rows or data_cols > table_cols: return False, f"Data ({data_rows}x{data_cols}) exceeds table dimensions ({table_rows}x{table_cols})", {} # Populate cells population_count = await self._populate_existing_table_cells( document_id, table_index, table_data ) metadata = { 'table_index': table_index, 'populated_cells': population_count, 'table_dimensions': f"{table_rows}x{table_cols}", 'data_dimensions': f"{data_rows}x{data_cols}" } return True, f"Successfully populated {population_count} cells in existing table", metadata except Exception as e: return False, f"Failed to populate existing table: {str(e)}", {} async def _populate_existing_table_cells( self, document_id: str, table_index: int, table_data: List[List[str]] ) -> int: """Populate cells in an existing table.""" population_count = 0 for row_idx, row_data in enumerate(table_data): for col_idx, cell_text in enumerate(row_data): if not cell_text: continue # Get fresh table structure for each cell tables = await self._get_document_tables(document_id) if table_index >= len(tables): break table = tables[table_index] cells = table.get('cells', []) if row_idx >= len(cells) or col_idx >= len(cells[row_idx]): continue cell = cells[row_idx][col_idx] # For existing tables, append to existing content cell_end = cell['end_index'] - 1 # Don't include cell end marker try: await asyncio.to_thread( self.service.documents().batchUpdate( documentId=document_id, body={'requests': [{ 'insertText': { 'location': {'index': cell_end}, 'text': cell_text } }]} ).execute ) population_count += 1 except Exception as e: logger.error(f"Failed to populate existing cell ({row_idx},{col_idx}): {str(e)}") return population_count

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ZatesloFL/google_workspace_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server