2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Google Docs Helper Functions
|
|
|
|
|
|
|
|
|
|
This module provides utility functions for common Google Docs operations
|
|
|
|
|
to simplify the implementation of document editing tools.
|
|
|
|
|
"""
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
import logging
|
2025-12-20 22:39:41 +01:00
|
|
|
from typing import Dict, Any, Optional
|
2025-08-09 17:57:34 -07:00
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
2025-12-15 15:00:26 +01:00
|
|
|
|
|
|
|
|
def _normalize_color(
|
2025-12-20 22:39:41 +01:00
|
|
|
color: Optional[str], param_name: str
|
2025-12-15 15:00:26 +01:00
|
|
|
) -> Optional[Dict[str, float]]:
|
2025-11-27 11:29:25 +01:00
|
|
|
"""
|
|
|
|
|
Normalize a user-supplied color into Docs API rgbColor format.
|
|
|
|
|
|
2025-12-20 22:39:41 +01:00
|
|
|
Supports only hex strings in the form "#RRGGBB".
|
2025-11-27 11:29:25 +01:00
|
|
|
"""
|
|
|
|
|
if color is None:
|
|
|
|
|
return None
|
|
|
|
|
|
2025-12-20 22:39:41 +01:00
|
|
|
if not isinstance(color, str):
|
|
|
|
|
raise ValueError(f"{param_name} must be a hex string like '#RRGGBB'")
|
|
|
|
|
|
|
|
|
|
if len(color) != 7 or not color.startswith("#"):
|
|
|
|
|
raise ValueError(f"{param_name} must be a hex string like '#RRGGBB'")
|
|
|
|
|
|
|
|
|
|
hex_color = color[1:]
|
|
|
|
|
if any(c not in "0123456789abcdefABCDEF" for c in hex_color):
|
|
|
|
|
raise ValueError(f"{param_name} must be a hex string like '#RRGGBB'")
|
|
|
|
|
|
|
|
|
|
r = int(hex_color[0:2], 16) / 255
|
|
|
|
|
g = int(hex_color[2:4], 16) / 255
|
|
|
|
|
b = int(hex_color[4:6], 16) / 255
|
|
|
|
|
return {"red": r, "green": g, "blue": b}
|
2025-11-27 11:29:25 +01:00
|
|
|
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
def build_text_style(
|
|
|
|
|
bold: bool = None,
|
|
|
|
|
italic: bool = None,
|
|
|
|
|
underline: bool = None,
|
|
|
|
|
font_size: int = None,
|
2025-11-27 11:29:25 +01:00
|
|
|
font_family: str = None,
|
2025-12-20 22:39:41 +01:00
|
|
|
text_color: str = None,
|
|
|
|
|
background_color: str = None,
|
2026-02-07 13:44:09 -08:00
|
|
|
link_url: str = None,
|
2025-08-10 15:56:18 -04:00
|
|
|
) -> tuple[Dict[str, Any], list[str]]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Build text style object for Google Docs API requests.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
bold: Whether text should be bold
|
|
|
|
|
italic: Whether text should be italic
|
|
|
|
|
underline: Whether text should be underlined
|
|
|
|
|
font_size: Font size in points
|
|
|
|
|
font_family: Font family name
|
2025-12-20 22:39:41 +01:00
|
|
|
text_color: Text color as hex string "#RRGGBB"
|
|
|
|
|
background_color: Background (highlight) color as hex string "#RRGGBB"
|
2026-02-07 13:44:09 -08:00
|
|
|
link_url: Hyperlink URL (http/https)
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Tuple of (text_style_dict, list_of_field_names)
|
|
|
|
|
"""
|
|
|
|
|
text_style = {}
|
|
|
|
|
fields = []
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if bold is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["bold"] = bold
|
|
|
|
|
fields.append("bold")
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if italic is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["italic"] = italic
|
|
|
|
|
fields.append("italic")
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if underline is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["underline"] = underline
|
|
|
|
|
fields.append("underline")
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if font_size is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["fontSize"] = {"magnitude": font_size, "unit": "PT"}
|
|
|
|
|
fields.append("fontSize")
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if font_family is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["weightedFontFamily"] = {"fontFamily": font_family}
|
|
|
|
|
fields.append("weightedFontFamily")
|
2025-11-27 11:29:25 +01:00
|
|
|
|
|
|
|
|
if text_color is not None:
|
|
|
|
|
rgb = _normalize_color(text_color, "text_color")
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["foregroundColor"] = {"color": {"rgbColor": rgb}}
|
|
|
|
|
fields.append("foregroundColor")
|
2025-11-27 11:29:25 +01:00
|
|
|
|
|
|
|
|
if background_color is not None:
|
|
|
|
|
rgb = _normalize_color(background_color, "background_color")
|
2025-12-13 13:49:28 -08:00
|
|
|
text_style["backgroundColor"] = {"color": {"rgbColor": rgb}}
|
|
|
|
|
fields.append("backgroundColor")
|
|
|
|
|
|
2026-02-07 13:44:09 -08:00
|
|
|
if link_url is not None:
|
|
|
|
|
text_style["link"] = {"url": link_url}
|
|
|
|
|
fields.append("link")
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
return text_style, fields
|
|
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-02-13 14:29:43 -08:00
|
|
|
def build_paragraph_style(
|
|
|
|
|
heading_level: int = None,
|
|
|
|
|
alignment: str = None,
|
|
|
|
|
line_spacing: float = None,
|
|
|
|
|
indent_first_line: float = None,
|
|
|
|
|
indent_start: float = None,
|
|
|
|
|
indent_end: float = None,
|
|
|
|
|
space_above: float = None,
|
|
|
|
|
space_below: float = None,
|
2026-03-12 20:34:36 +01:00
|
|
|
named_style_type: str = None,
|
2026-02-13 14:29:43 -08:00
|
|
|
) -> tuple[Dict[str, Any], list[str]]:
|
|
|
|
|
"""
|
|
|
|
|
Build paragraph style object for Google Docs API requests.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
heading_level: Heading level 0-6 (0 = NORMAL_TEXT, 1-6 = HEADING_N)
|
|
|
|
|
alignment: Text alignment - 'START', 'CENTER', 'END', or 'JUSTIFIED'
|
|
|
|
|
line_spacing: Line spacing multiplier (1.0 = single, 2.0 = double)
|
|
|
|
|
indent_first_line: First line indent in points
|
|
|
|
|
indent_start: Left/start indent in points
|
|
|
|
|
indent_end: Right/end indent in points
|
|
|
|
|
space_above: Space above paragraph in points
|
|
|
|
|
space_below: Space below paragraph in points
|
2026-03-12 20:34:36 +01:00
|
|
|
named_style_type: Direct named style (TITLE, SUBTITLE, HEADING_1..6, NORMAL_TEXT).
|
|
|
|
|
Takes precedence over heading_level when both are provided.
|
2026-02-13 14:29:43 -08:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Tuple of (paragraph_style_dict, list_of_field_names)
|
|
|
|
|
"""
|
|
|
|
|
paragraph_style = {}
|
|
|
|
|
fields = []
|
|
|
|
|
|
2026-03-12 20:34:36 +01:00
|
|
|
if named_style_type is not None:
|
|
|
|
|
valid_styles = [
|
|
|
|
|
"NORMAL_TEXT", "TITLE", "SUBTITLE",
|
|
|
|
|
"HEADING_1", "HEADING_2", "HEADING_3",
|
|
|
|
|
"HEADING_4", "HEADING_5", "HEADING_6",
|
|
|
|
|
]
|
|
|
|
|
if named_style_type not in valid_styles:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Invalid named_style_type '{named_style_type}'. "
|
|
|
|
|
f"Must be one of: {', '.join(valid_styles)}"
|
|
|
|
|
)
|
|
|
|
|
paragraph_style["namedStyleType"] = named_style_type
|
|
|
|
|
fields.append("namedStyleType")
|
|
|
|
|
elif heading_level is not None:
|
2026-02-13 14:29:43 -08:00
|
|
|
if heading_level < 0 or heading_level > 6:
|
|
|
|
|
raise ValueError("heading_level must be between 0 (normal text) and 6")
|
|
|
|
|
if heading_level == 0:
|
|
|
|
|
paragraph_style["namedStyleType"] = "NORMAL_TEXT"
|
|
|
|
|
else:
|
|
|
|
|
paragraph_style["namedStyleType"] = f"HEADING_{heading_level}"
|
|
|
|
|
fields.append("namedStyleType")
|
|
|
|
|
|
|
|
|
|
if alignment is not None:
|
|
|
|
|
valid_alignments = ["START", "CENTER", "END", "JUSTIFIED"]
|
|
|
|
|
alignment_upper = alignment.upper()
|
|
|
|
|
if alignment_upper not in valid_alignments:
|
|
|
|
|
raise ValueError(
|
|
|
|
|
f"Invalid alignment '{alignment}'. Must be one of: {valid_alignments}"
|
|
|
|
|
)
|
|
|
|
|
paragraph_style["alignment"] = alignment_upper
|
|
|
|
|
fields.append("alignment")
|
|
|
|
|
|
|
|
|
|
if line_spacing is not None:
|
|
|
|
|
if line_spacing <= 0:
|
|
|
|
|
raise ValueError("line_spacing must be positive")
|
|
|
|
|
paragraph_style["lineSpacing"] = line_spacing * 100
|
|
|
|
|
fields.append("lineSpacing")
|
|
|
|
|
|
|
|
|
|
if indent_first_line is not None:
|
|
|
|
|
paragraph_style["indentFirstLine"] = {
|
|
|
|
|
"magnitude": indent_first_line,
|
|
|
|
|
"unit": "PT",
|
|
|
|
|
}
|
|
|
|
|
fields.append("indentFirstLine")
|
|
|
|
|
|
|
|
|
|
if indent_start is not None:
|
|
|
|
|
paragraph_style["indentStart"] = {"magnitude": indent_start, "unit": "PT"}
|
|
|
|
|
fields.append("indentStart")
|
|
|
|
|
|
|
|
|
|
if indent_end is not None:
|
|
|
|
|
paragraph_style["indentEnd"] = {"magnitude": indent_end, "unit": "PT"}
|
|
|
|
|
fields.append("indentEnd")
|
|
|
|
|
|
|
|
|
|
if space_above is not None:
|
|
|
|
|
paragraph_style["spaceAbove"] = {"magnitude": space_above, "unit": "PT"}
|
|
|
|
|
fields.append("spaceAbove")
|
|
|
|
|
|
|
|
|
|
if space_below is not None:
|
|
|
|
|
paragraph_style["spaceBelow"] = {"magnitude": space_below, "unit": "PT"}
|
|
|
|
|
fields.append("spaceBelow")
|
|
|
|
|
|
|
|
|
|
return paragraph_style, fields
|
|
|
|
|
|
|
|
|
|
|
2026-03-01 10:40:19 -08:00
|
|
|
def create_insert_text_request(
|
|
|
|
|
index: int, text: str, tab_id: Optional[str] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Create an insertText request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
index: Position to insert text
|
|
|
|
|
text: Text to insert
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the insertText request
|
|
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
location = {"index": index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
location["tabId"] = tab_id
|
|
|
|
|
return {"insertText": {"location": location, "text": text}}
|
2025-08-09 17:57:34 -07:00
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
|
|
|
|
def create_insert_text_segment_request(
|
2026-03-01 10:40:19 -08:00
|
|
|
index: int, text: str, segment_id: str, tab_id: Optional[str] = None
|
2025-12-13 13:49:28 -08:00
|
|
|
) -> Dict[str, Any]:
|
2025-08-14 18:38:37 -07:00
|
|
|
"""
|
|
|
|
|
Create an insertText request for Google Docs API with segmentId (for headers/footers).
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-14 18:38:37 -07:00
|
|
|
Args:
|
|
|
|
|
index: Position to insert text
|
|
|
|
|
text: Text to insert
|
|
|
|
|
segment_id: Segment ID (for targeting headers/footers)
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-14 18:38:37 -07:00
|
|
|
Returns:
|
2026-03-01 10:40:19 -08:00
|
|
|
Dictionary representing the insertText request with segmentId and optional tabId
|
2025-08-14 18:38:37 -07:00
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
location = {"segmentId": segment_id, "index": index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
location["tabId"] = tab_id
|
2025-08-14 18:38:37 -07:00
|
|
|
return {
|
2025-12-13 13:49:28 -08:00
|
|
|
"insertText": {
|
2026-03-01 10:40:19 -08:00
|
|
|
"location": location,
|
2025-12-13 13:49:28 -08:00
|
|
|
"text": text,
|
2025-08-14 18:38:37 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-03-01 10:40:19 -08:00
|
|
|
def create_delete_range_request(
|
|
|
|
|
start_index: int, end_index: int, tab_id: Optional[str] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Create a deleteContentRange request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
start_index: Start position of content to delete
|
|
|
|
|
end_index: End position of content to delete
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the deleteContentRange request
|
|
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
range_obj = {"startIndex": start_index, "endIndex": end_index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
range_obj["tabId"] = tab_id
|
|
|
|
|
return {"deleteContentRange": {"range": range_obj}}
|
2025-08-09 17:57:34 -07:00
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
def create_format_text_request(
|
2025-12-13 13:49:28 -08:00
|
|
|
start_index: int,
|
2025-08-09 17:57:34 -07:00
|
|
|
end_index: int,
|
|
|
|
|
bold: bool = None,
|
|
|
|
|
italic: bool = None,
|
|
|
|
|
underline: bool = None,
|
|
|
|
|
font_size: int = None,
|
2025-11-27 11:29:25 +01:00
|
|
|
font_family: str = None,
|
2025-12-20 22:39:41 +01:00
|
|
|
text_color: str = None,
|
|
|
|
|
background_color: str = None,
|
2026-02-07 13:44:09 -08:00
|
|
|
link_url: str = None,
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional[str] = None,
|
2025-08-09 17:57:34 -07:00
|
|
|
) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""
|
|
|
|
|
Create an updateTextStyle request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
start_index: Start position of text to format
|
|
|
|
|
end_index: End position of text to format
|
|
|
|
|
bold: Whether text should be bold
|
|
|
|
|
italic: Whether text should be italic
|
|
|
|
|
underline: Whether text should be underlined
|
|
|
|
|
font_size: Font size in points
|
|
|
|
|
font_family: Font family name
|
2025-12-20 22:39:41 +01:00
|
|
|
text_color: Text color as hex string "#RRGGBB"
|
|
|
|
|
background_color: Background (highlight) color as hex string "#RRGGBB"
|
2026-02-07 13:44:09 -08:00
|
|
|
link_url: Hyperlink URL (http/https)
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the updateTextStyle request, or None if no styles provided
|
|
|
|
|
"""
|
2025-11-27 11:29:25 +01:00
|
|
|
text_style, fields = build_text_style(
|
2026-02-07 13:44:09 -08:00
|
|
|
bold,
|
|
|
|
|
italic,
|
|
|
|
|
underline,
|
|
|
|
|
font_size,
|
|
|
|
|
font_family,
|
|
|
|
|
text_color,
|
|
|
|
|
background_color,
|
|
|
|
|
link_url,
|
2025-11-27 11:29:25 +01:00
|
|
|
)
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if not text_style:
|
|
|
|
|
return None
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-03-01 10:40:19 -08:00
|
|
|
range_obj = {"startIndex": start_index, "endIndex": end_index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
range_obj["tabId"] = tab_id
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
return {
|
2025-12-13 13:49:28 -08:00
|
|
|
"updateTextStyle": {
|
2026-03-01 10:40:19 -08:00
|
|
|
"range": range_obj,
|
2025-12-13 13:49:28 -08:00
|
|
|
"textStyle": text_style,
|
|
|
|
|
"fields": ",".join(fields),
|
2025-08-09 17:57:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-02-13 14:29:43 -08:00
|
|
|
def create_update_paragraph_style_request(
|
|
|
|
|
start_index: int,
|
|
|
|
|
end_index: int,
|
|
|
|
|
heading_level: int = None,
|
|
|
|
|
alignment: str = None,
|
|
|
|
|
line_spacing: float = None,
|
|
|
|
|
indent_first_line: float = None,
|
|
|
|
|
indent_start: float = None,
|
|
|
|
|
indent_end: float = None,
|
|
|
|
|
space_above: float = None,
|
|
|
|
|
space_below: float = None,
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional[str] = None,
|
2026-03-12 20:34:36 +01:00
|
|
|
named_style_type: str = None,
|
2026-02-13 14:29:43 -08:00
|
|
|
) -> Optional[Dict[str, Any]]:
|
|
|
|
|
"""
|
|
|
|
|
Create an updateParagraphStyle request for Google Docs API.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
start_index: Start position of paragraph range
|
|
|
|
|
end_index: End position of paragraph range
|
|
|
|
|
heading_level: Heading level 0-6 (0 = NORMAL_TEXT, 1-6 = HEADING_N)
|
|
|
|
|
alignment: Text alignment - 'START', 'CENTER', 'END', or 'JUSTIFIED'
|
|
|
|
|
line_spacing: Line spacing multiplier (1.0 = single, 2.0 = double)
|
|
|
|
|
indent_first_line: First line indent in points
|
|
|
|
|
indent_start: Left/start indent in points
|
|
|
|
|
indent_end: Right/end indent in points
|
|
|
|
|
space_above: Space above paragraph in points
|
|
|
|
|
space_below: Space below paragraph in points
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2026-03-12 20:34:36 +01:00
|
|
|
named_style_type: Direct named style (TITLE, SUBTITLE, HEADING_1..6, NORMAL_TEXT)
|
2026-02-13 14:29:43 -08:00
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the updateParagraphStyle request, or None if no styles provided
|
|
|
|
|
"""
|
|
|
|
|
paragraph_style, fields = build_paragraph_style(
|
|
|
|
|
heading_level,
|
|
|
|
|
alignment,
|
|
|
|
|
line_spacing,
|
|
|
|
|
indent_first_line,
|
|
|
|
|
indent_start,
|
|
|
|
|
indent_end,
|
|
|
|
|
space_above,
|
|
|
|
|
space_below,
|
2026-03-12 20:34:36 +01:00
|
|
|
named_style_type,
|
2026-02-13 14:29:43 -08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if not paragraph_style:
|
|
|
|
|
return None
|
|
|
|
|
|
2026-03-01 10:40:19 -08:00
|
|
|
range_obj = {"startIndex": start_index, "endIndex": end_index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
range_obj["tabId"] = tab_id
|
|
|
|
|
|
2026-02-13 14:29:43 -08:00
|
|
|
return {
|
|
|
|
|
"updateParagraphStyle": {
|
2026-03-01 10:40:19 -08:00
|
|
|
"range": range_obj,
|
2026-02-13 14:29:43 -08:00
|
|
|
"paragraphStyle": paragraph_style,
|
|
|
|
|
"fields": ",".join(fields),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
def create_find_replace_request(
|
2026-03-03 17:36:21 -05:00
|
|
|
find_text: str,
|
|
|
|
|
replace_text: str,
|
|
|
|
|
match_case: bool = False,
|
|
|
|
|
tab_id: Optional[str] = None,
|
2025-08-09 17:57:34 -07:00
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Create a replaceAllText request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
find_text: Text to find
|
|
|
|
|
replace_text: Text to replace with
|
|
|
|
|
match_case: Whether to match case exactly
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the replaceAllText request
|
|
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
request = {
|
2025-12-13 13:49:28 -08:00
|
|
|
"replaceAllText": {
|
|
|
|
|
"containsText": {"text": find_text, "matchCase": match_case},
|
|
|
|
|
"replaceText": replace_text,
|
2025-08-09 17:57:34 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-01 10:40:19 -08:00
|
|
|
if tab_id:
|
|
|
|
|
request["replaceAllText"]["tabsCriteria"] = {"tabIds": [tab_id]}
|
|
|
|
|
return request
|
2025-08-09 17:57:34 -07:00
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-03-01 10:40:19 -08:00
|
|
|
def create_insert_table_request(
|
|
|
|
|
index: int, rows: int, columns: int, tab_id: Optional[str] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Create an insertTable request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
index: Position to insert table
|
|
|
|
|
rows: Number of rows
|
|
|
|
|
columns: Number of columns
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the insertTable request
|
|
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
location = {"index": index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
location["tabId"] = tab_id
|
|
|
|
|
return {"insertTable": {"location": location, "rows": rows, "columns": columns}}
|
2025-08-09 17:57:34 -07:00
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-03-01 10:40:19 -08:00
|
|
|
def create_insert_page_break_request(
|
|
|
|
|
index: int, tab_id: Optional[str] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Create an insertPageBreak request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
index: Position to insert page break
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the insertPageBreak request
|
|
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
location = {"index": index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
location["tabId"] = tab_id
|
|
|
|
|
return {"insertPageBreak": {"location": location}}
|
|
|
|
|
|
|
|
|
|
|
2026-03-03 17:36:21 -05:00
|
|
|
def create_insert_doc_tab_request(
|
|
|
|
|
title: str, index: int, parent_tab_id: Optional[str] = None
|
|
|
|
|
) -> Dict[str, Any]:
|
2026-03-01 10:40:19 -08:00
|
|
|
"""
|
|
|
|
|
Create an addDocumentTab request for Google Docs API.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
title: Title of the new tab
|
|
|
|
|
index: Position to insert the tab
|
|
|
|
|
parent_tab_id: Optional ID of the parent tab to nest under
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the addDocumentTab request
|
|
|
|
|
"""
|
|
|
|
|
tab_properties: Dict[str, Any] = {
|
|
|
|
|
"title": title,
|
|
|
|
|
"index": index,
|
|
|
|
|
}
|
|
|
|
|
if parent_tab_id:
|
|
|
|
|
tab_properties["parentTabId"] = parent_tab_id
|
|
|
|
|
return {
|
|
|
|
|
"addDocumentTab": {
|
|
|
|
|
"tabProperties": tab_properties,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_delete_doc_tab_request(tab_id: str) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Create a deleteDocumentTab request for Google Docs API.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
tab_id: ID of the tab to delete
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the deleteDocumentTab request
|
|
|
|
|
"""
|
|
|
|
|
return {"deleteTab": {"tabId": tab_id}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_update_doc_tab_request(tab_id: str, title: str) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Create an updateDocumentTab request for Google Docs API.
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
tab_id: ID of the tab to update
|
|
|
|
|
title: New title for the tab
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the updateDocumentTab request
|
|
|
|
|
"""
|
|
|
|
|
return {
|
|
|
|
|
"updateDocumentTabProperties": {
|
|
|
|
|
"tabProperties": {
|
|
|
|
|
"tabId": tab_id,
|
|
|
|
|
"title": title,
|
|
|
|
|
},
|
|
|
|
|
"fields": "title",
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
|
|
|
|
|
def create_insert_image_request(
|
2026-03-01 10:40:19 -08:00
|
|
|
index: int,
|
|
|
|
|
image_uri: str,
|
|
|
|
|
width: int = None,
|
|
|
|
|
height: int = None,
|
|
|
|
|
tab_id: Optional[str] = None,
|
2025-08-09 17:57:34 -07:00
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
"""
|
|
|
|
|
Create an insertInlineImage request for Google Docs API.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
index: Position to insert image
|
|
|
|
|
image_uri: URI of the image (Drive URL or public URL)
|
|
|
|
|
width: Image width in points
|
|
|
|
|
height: Image height in points
|
2026-03-01 10:40:19 -08:00
|
|
|
tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Dictionary representing the insertInlineImage request
|
|
|
|
|
"""
|
2026-03-01 10:40:19 -08:00
|
|
|
location = {"index": index}
|
|
|
|
|
if tab_id:
|
|
|
|
|
location["tabId"] = tab_id
|
|
|
|
|
|
|
|
|
|
request = {"insertInlineImage": {"location": location, "uri": image_uri}}
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
# Add size properties if specified
|
|
|
|
|
object_size = {}
|
|
|
|
|
if width is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
object_size["width"] = {"magnitude": width, "unit": "PT"}
|
2025-08-09 17:57:34 -07:00
|
|
|
if height is not None:
|
2025-12-13 13:49:28 -08:00
|
|
|
object_size["height"] = {"magnitude": height, "unit": "PT"}
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if object_size:
|
2025-12-13 13:49:28 -08:00
|
|
|
request["insertInlineImage"]["objectSize"] = object_size
|
|
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
return request
|
|
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
def create_bullet_list_request(
|
2026-02-08 15:42:58 -05:00
|
|
|
start_index: int,
|
|
|
|
|
end_index: int,
|
|
|
|
|
list_type: str = "UNORDERED",
|
|
|
|
|
nesting_level: int = None,
|
2026-02-08 18:15:36 -05:00
|
|
|
paragraph_start_indices: Optional[list[int]] = None,
|
2026-03-01 10:40:19 -08:00
|
|
|
doc_tab_id: Optional[str] = None,
|
2026-02-08 17:38:20 -05:00
|
|
|
) -> list[Dict[str, Any]]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
2026-02-08 17:38:20 -05:00
|
|
|
Create requests to apply bullet list formatting with optional nesting.
|
|
|
|
|
|
2026-02-08 18:15:36 -05:00
|
|
|
Google Docs infers list nesting from leading tab characters. To set a nested
|
|
|
|
|
level, this helper inserts literal tab characters before each targeted
|
|
|
|
|
paragraph, then calls createParagraphBullets. This is a Docs API workaround
|
|
|
|
|
and does temporarily mutate content/index positions while the batch executes.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
start_index: Start of text range to convert to list
|
|
|
|
|
end_index: End of text range to convert to list
|
|
|
|
|
list_type: Type of list ("UNORDERED" or "ORDERED")
|
2026-02-08 17:38:20 -05:00
|
|
|
nesting_level: Nesting level (0-8, where 0 is top level). If None or 0, no tabs added.
|
2026-02-08 18:15:36 -05:00
|
|
|
paragraph_start_indices: Optional paragraph start positions for ranges with
|
|
|
|
|
multiple paragraphs. If omitted, only start_index is tab-prefixed.
|
2026-03-03 17:36:21 -05:00
|
|
|
doc_tab_id: Optional ID of the tab to target
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
2026-02-08 18:15:36 -05:00
|
|
|
List of request dictionaries (insertText for nesting tabs if needed,
|
|
|
|
|
then createParagraphBullets)
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
bullet_preset = (
|
2025-12-13 13:49:28 -08:00
|
|
|
"BULLET_DISC_CIRCLE_SQUARE"
|
|
|
|
|
if list_type == "UNORDERED"
|
|
|
|
|
else "NUMBERED_DECIMAL_ALPHA_ROMAN"
|
2025-08-09 17:57:34 -07:00
|
|
|
)
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2026-02-08 17:38:20 -05:00
|
|
|
# Validate nesting level
|
2026-02-08 15:42:58 -05:00
|
|
|
if nesting_level is not None:
|
2026-02-08 16:09:37 -05:00
|
|
|
if not isinstance(nesting_level, int):
|
|
|
|
|
raise ValueError("nesting_level must be an integer between 0 and 8")
|
|
|
|
|
if nesting_level < 0 or nesting_level > 8:
|
|
|
|
|
raise ValueError("nesting_level must be between 0 and 8")
|
2026-02-08 15:42:58 -05:00
|
|
|
|
2026-02-08 17:38:20 -05:00
|
|
|
requests = []
|
|
|
|
|
|
2026-02-08 18:15:36 -05:00
|
|
|
# Insert tabs for nesting if needed (nesting_level > 0).
|
|
|
|
|
# For multi-paragraph ranges, callers should provide paragraph_start_indices.
|
2026-02-08 17:38:20 -05:00
|
|
|
if nesting_level and nesting_level > 0:
|
|
|
|
|
tabs = "\t" * nesting_level
|
2026-02-08 18:15:36 -05:00
|
|
|
paragraph_starts = paragraph_start_indices or [start_index]
|
|
|
|
|
paragraph_starts = sorted(set(paragraph_starts))
|
|
|
|
|
|
|
|
|
|
if any(not isinstance(idx, int) for idx in paragraph_starts):
|
|
|
|
|
raise ValueError("paragraph_start_indices must contain only integers")
|
|
|
|
|
|
|
|
|
|
original_start = start_index
|
|
|
|
|
original_end = end_index
|
|
|
|
|
inserted_char_count = 0
|
|
|
|
|
|
|
|
|
|
for paragraph_start in paragraph_starts:
|
|
|
|
|
adjusted_start = paragraph_start + inserted_char_count
|
2026-03-03 17:36:21 -05:00
|
|
|
requests.append(
|
|
|
|
|
create_insert_text_request(adjusted_start, tabs, doc_tab_id)
|
|
|
|
|
)
|
2026-02-08 18:15:36 -05:00
|
|
|
inserted_char_count += nesting_level
|
|
|
|
|
|
|
|
|
|
# Keep createParagraphBullets range aligned to the same logical content.
|
|
|
|
|
start_index += (
|
|
|
|
|
sum(1 for idx in paragraph_starts if idx < original_start) * nesting_level
|
|
|
|
|
)
|
|
|
|
|
end_index += (
|
|
|
|
|
sum(1 for idx in paragraph_starts if idx < original_end) * nesting_level
|
2026-02-08 17:38:20 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Create the bullet list
|
2026-03-01 10:40:19 -08:00
|
|
|
range_obj = {"startIndex": start_index, "endIndex": end_index}
|
|
|
|
|
if doc_tab_id:
|
|
|
|
|
range_obj["tabId"] = doc_tab_id
|
|
|
|
|
|
2026-02-08 17:38:20 -05:00
|
|
|
requests.append(
|
|
|
|
|
{
|
|
|
|
|
"createParagraphBullets": {
|
2026-03-01 10:40:19 -08:00
|
|
|
"range": range_obj,
|
2026-02-08 17:38:20 -05:00
|
|
|
"bulletPreset": bullet_preset,
|
|
|
|
|
}
|
2025-08-09 17:57:34 -07:00
|
|
|
}
|
2026-02-08 17:38:20 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return requests
|
2025-08-09 17:57:34 -07:00
|
|
|
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-12-20 22:39:41 +01:00
|
|
|
def validate_operation(operation: Dict[str, Any]) -> tuple[bool, str]:
|
2025-08-09 17:57:34 -07:00
|
|
|
"""
|
|
|
|
|
Validate a batch operation dictionary.
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Args:
|
|
|
|
|
operation: Operation dictionary to validate
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
Returns:
|
|
|
|
|
Tuple of (is_valid, error_message)
|
|
|
|
|
"""
|
2025-12-13 13:49:28 -08:00
|
|
|
op_type = operation.get("type")
|
2025-08-09 17:57:34 -07:00
|
|
|
if not op_type:
|
|
|
|
|
return False, "Missing 'type' field"
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
# Validate required fields for each operation type
|
|
|
|
|
required_fields = {
|
2025-12-13 13:49:28 -08:00
|
|
|
"insert_text": ["index", "text"],
|
|
|
|
|
"delete_text": ["start_index", "end_index"],
|
|
|
|
|
"replace_text": ["start_index", "end_index", "text"],
|
|
|
|
|
"format_text": ["start_index", "end_index"],
|
2026-02-13 14:29:43 -08:00
|
|
|
"update_paragraph_style": ["start_index", "end_index"],
|
2025-12-13 13:49:28 -08:00
|
|
|
"insert_table": ["index", "rows", "columns"],
|
|
|
|
|
"insert_page_break": ["index"],
|
|
|
|
|
"find_replace": ["find_text", "replace_text"],
|
2026-03-01 10:40:19 -08:00
|
|
|
"insert_doc_tab": ["title", "index"],
|
|
|
|
|
"delete_doc_tab": ["tab_id"],
|
|
|
|
|
"update_doc_tab": ["tab_id", "title"],
|
2025-08-09 17:57:34 -07:00
|
|
|
}
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
if op_type not in required_fields:
|
2025-08-10 14:35:56 -04:00
|
|
|
return False, f"Unsupported operation type: {op_type or 'None'}"
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
for field in required_fields[op_type]:
|
|
|
|
|
if field not in operation:
|
|
|
|
|
return False, f"Missing required field: {field}"
|
2025-12-13 13:49:28 -08:00
|
|
|
|
2025-08-09 17:57:34 -07:00
|
|
|
return True, ""
|