|
| 1 | +""" |
| 2 | +Google Docs Helper Functions |
| 3 | +
|
| 4 | +This module provides utility functions for common Google Docs operations |
| 5 | +to simplify the implementation of document editing tools. |
| 6 | +""" |
| 7 | +import logging |
| 8 | +from typing import Dict, Any, Optional, Tuple |
| 9 | + |
| 10 | +logger = logging.getLogger(__name__) |
| 11 | + |
| 12 | +def build_text_style( |
| 13 | + bold: bool = None, |
| 14 | + italic: bool = None, |
| 15 | + underline: bool = None, |
| 16 | + font_size: int = None, |
| 17 | + font_family: str = None |
| 18 | +) -> tuple[Dict[str, Any], list[str]]: |
| 19 | + """ |
| 20 | + Build text style object for Google Docs API requests. |
| 21 | + |
| 22 | + Args: |
| 23 | + bold: Whether text should be bold |
| 24 | + italic: Whether text should be italic |
| 25 | + underline: Whether text should be underlined |
| 26 | + font_size: Font size in points |
| 27 | + font_family: Font family name |
| 28 | + |
| 29 | + Returns: |
| 30 | + Tuple of (text_style_dict, list_of_field_names) |
| 31 | + """ |
| 32 | + text_style = {} |
| 33 | + fields = [] |
| 34 | + |
| 35 | + if bold is not None: |
| 36 | + text_style['bold'] = bold |
| 37 | + fields.append('bold') |
| 38 | + |
| 39 | + if italic is not None: |
| 40 | + text_style['italic'] = italic |
| 41 | + fields.append('italic') |
| 42 | + |
| 43 | + if underline is not None: |
| 44 | + text_style['underline'] = underline |
| 45 | + fields.append('underline') |
| 46 | + |
| 47 | + if font_size is not None: |
| 48 | + text_style['fontSize'] = {'magnitude': font_size, 'unit': 'PT'} |
| 49 | + fields.append('fontSize') |
| 50 | + |
| 51 | + if font_family is not None: |
| 52 | + text_style['weightedFontFamily'] = {'fontFamily': font_family} |
| 53 | + fields.append('weightedFontFamily') |
| 54 | + |
| 55 | + return text_style, fields |
| 56 | + |
| 57 | +def create_insert_text_request(index: int, text: str) -> Dict[str, Any]: |
| 58 | + """ |
| 59 | + Create an insertText request for Google Docs API. |
| 60 | + |
| 61 | + Args: |
| 62 | + index: Position to insert text |
| 63 | + text: Text to insert |
| 64 | + |
| 65 | + Returns: |
| 66 | + Dictionary representing the insertText request |
| 67 | + """ |
| 68 | + return { |
| 69 | + 'insertText': { |
| 70 | + 'location': {'index': index}, |
| 71 | + 'text': text |
| 72 | + } |
| 73 | + } |
| 74 | + |
| 75 | +def create_delete_range_request(start_index: int, end_index: int) -> Dict[str, Any]: |
| 76 | + """ |
| 77 | + Create a deleteContentRange request for Google Docs API. |
| 78 | + |
| 79 | + Args: |
| 80 | + start_index: Start position of content to delete |
| 81 | + end_index: End position of content to delete |
| 82 | + |
| 83 | + Returns: |
| 84 | + Dictionary representing the deleteContentRange request |
| 85 | + """ |
| 86 | + return { |
| 87 | + 'deleteContentRange': { |
| 88 | + 'range': { |
| 89 | + 'startIndex': start_index, |
| 90 | + 'endIndex': end_index |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | + |
| 95 | +def create_format_text_request( |
| 96 | + start_index: int, |
| 97 | + end_index: int, |
| 98 | + bold: bool = None, |
| 99 | + italic: bool = None, |
| 100 | + underline: bool = None, |
| 101 | + font_size: int = None, |
| 102 | + font_family: str = None |
| 103 | +) -> Optional[Dict[str, Any]]: |
| 104 | + """ |
| 105 | + Create an updateTextStyle request for Google Docs API. |
| 106 | + |
| 107 | + Args: |
| 108 | + start_index: Start position of text to format |
| 109 | + end_index: End position of text to format |
| 110 | + bold: Whether text should be bold |
| 111 | + italic: Whether text should be italic |
| 112 | + underline: Whether text should be underlined |
| 113 | + font_size: Font size in points |
| 114 | + font_family: Font family name |
| 115 | + |
| 116 | + Returns: |
| 117 | + Dictionary representing the updateTextStyle request, or None if no styles provided |
| 118 | + """ |
| 119 | + text_style, fields = build_text_style(bold, italic, underline, font_size, font_family) |
| 120 | + |
| 121 | + if not text_style: |
| 122 | + return None |
| 123 | + |
| 124 | + return { |
| 125 | + 'updateTextStyle': { |
| 126 | + 'range': { |
| 127 | + 'startIndex': start_index, |
| 128 | + 'endIndex': end_index |
| 129 | + }, |
| 130 | + 'textStyle': text_style, |
| 131 | + 'fields': ','.join(fields) |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | +def create_find_replace_request( |
| 136 | + find_text: str, |
| 137 | + replace_text: str, |
| 138 | + match_case: bool = False |
| 139 | +) -> Dict[str, Any]: |
| 140 | + """ |
| 141 | + Create a replaceAllText request for Google Docs API. |
| 142 | + |
| 143 | + Args: |
| 144 | + find_text: Text to find |
| 145 | + replace_text: Text to replace with |
| 146 | + match_case: Whether to match case exactly |
| 147 | + |
| 148 | + Returns: |
| 149 | + Dictionary representing the replaceAllText request |
| 150 | + """ |
| 151 | + return { |
| 152 | + 'replaceAllText': { |
| 153 | + 'containsText': { |
| 154 | + 'text': find_text, |
| 155 | + 'matchCase': match_case |
| 156 | + }, |
| 157 | + 'replaceText': replace_text |
| 158 | + } |
| 159 | + } |
| 160 | + |
| 161 | +def create_insert_table_request(index: int, rows: int, columns: int) -> Dict[str, Any]: |
| 162 | + """ |
| 163 | + Create an insertTable request for Google Docs API. |
| 164 | + |
| 165 | + Args: |
| 166 | + index: Position to insert table |
| 167 | + rows: Number of rows |
| 168 | + columns: Number of columns |
| 169 | + |
| 170 | + Returns: |
| 171 | + Dictionary representing the insertTable request |
| 172 | + """ |
| 173 | + return { |
| 174 | + 'insertTable': { |
| 175 | + 'location': {'index': index}, |
| 176 | + 'rows': rows, |
| 177 | + 'columns': columns |
| 178 | + } |
| 179 | + } |
| 180 | + |
| 181 | +def create_insert_page_break_request(index: int) -> Dict[str, Any]: |
| 182 | + """ |
| 183 | + Create an insertPageBreak request for Google Docs API. |
| 184 | + |
| 185 | + Args: |
| 186 | + index: Position to insert page break |
| 187 | + |
| 188 | + Returns: |
| 189 | + Dictionary representing the insertPageBreak request |
| 190 | + """ |
| 191 | + return { |
| 192 | + 'insertPageBreak': { |
| 193 | + 'location': {'index': index} |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | +def create_insert_image_request( |
| 198 | + index: int, |
| 199 | + image_uri: str, |
| 200 | + width: int = None, |
| 201 | + height: int = None |
| 202 | +) -> Dict[str, Any]: |
| 203 | + """ |
| 204 | + Create an insertInlineImage request for Google Docs API. |
| 205 | + |
| 206 | + Args: |
| 207 | + index: Position to insert image |
| 208 | + image_uri: URI of the image (Drive URL or public URL) |
| 209 | + width: Image width in points |
| 210 | + height: Image height in points |
| 211 | + |
| 212 | + Returns: |
| 213 | + Dictionary representing the insertInlineImage request |
| 214 | + """ |
| 215 | + request = { |
| 216 | + 'insertInlineImage': { |
| 217 | + 'location': {'index': index}, |
| 218 | + 'uri': image_uri |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + # Add size properties if specified |
| 223 | + object_size = {} |
| 224 | + if width is not None: |
| 225 | + object_size['width'] = {'magnitude': width, 'unit': 'PT'} |
| 226 | + if height is not None: |
| 227 | + object_size['height'] = {'magnitude': height, 'unit': 'PT'} |
| 228 | + |
| 229 | + if object_size: |
| 230 | + request['insertInlineImage']['objectSize'] = object_size |
| 231 | + |
| 232 | + return request |
| 233 | + |
| 234 | +def create_bullet_list_request( |
| 235 | + start_index: int, |
| 236 | + end_index: int, |
| 237 | + list_type: str = "UNORDERED" |
| 238 | +) -> Dict[str, Any]: |
| 239 | + """ |
| 240 | + Create a createParagraphBullets request for Google Docs API. |
| 241 | + |
| 242 | + Args: |
| 243 | + start_index: Start of text range to convert to list |
| 244 | + end_index: End of text range to convert to list |
| 245 | + list_type: Type of list ("UNORDERED" or "ORDERED") |
| 246 | + |
| 247 | + Returns: |
| 248 | + Dictionary representing the createParagraphBullets request |
| 249 | + """ |
| 250 | + bullet_preset = ( |
| 251 | + 'BULLET_DISC_CIRCLE_SQUARE' |
| 252 | + if list_type == "UNORDERED" |
| 253 | + else 'NUMBERED_DECIMAL_ALPHA_ROMAN' |
| 254 | + ) |
| 255 | + |
| 256 | + return { |
| 257 | + 'createParagraphBullets': { |
| 258 | + 'range': { |
| 259 | + 'startIndex': start_index, |
| 260 | + 'endIndex': end_index |
| 261 | + }, |
| 262 | + 'bulletPreset': bullet_preset |
| 263 | + } |
| 264 | + } |
| 265 | + |
| 266 | +def validate_operation(operation: Dict[str, Any]) -> Tuple[bool, str]: |
| 267 | + """ |
| 268 | + Validate a batch operation dictionary. |
| 269 | + |
| 270 | + Args: |
| 271 | + operation: Operation dictionary to validate |
| 272 | + |
| 273 | + Returns: |
| 274 | + Tuple of (is_valid, error_message) |
| 275 | + """ |
| 276 | + op_type = operation.get('type') |
| 277 | + if not op_type: |
| 278 | + return False, "Missing 'type' field" |
| 279 | + |
| 280 | + # Validate required fields for each operation type |
| 281 | + required_fields = { |
| 282 | + 'insert_text': ['index', 'text'], |
| 283 | + 'delete_text': ['start_index', 'end_index'], |
| 284 | + 'replace_text': ['start_index', 'end_index', 'text'], |
| 285 | + 'format_text': ['start_index', 'end_index'], |
| 286 | + 'insert_table': ['index', 'rows', 'columns'], |
| 287 | + 'insert_page_break': ['index'], |
| 288 | + 'find_replace': ['find_text', 'replace_text'] |
| 289 | + } |
| 290 | + |
| 291 | + if op_type not in required_fields: |
| 292 | + return False, f"Unsupported operation type: {op_type or 'None'}" |
| 293 | + |
| 294 | + for field in required_fields[op_type]: |
| 295 | + if field not in operation: |
| 296 | + return False, f"Missing required field: {field}" |
| 297 | + |
| 298 | + return True, "" |
| 299 | + |
0 commit comments