This commit is contained in:
Taylor Wilsdon
2026-02-17 14:49:49 -05:00
38 changed files with 2232 additions and 489 deletions

View File

@@ -13,6 +13,7 @@ from gdocs.docs_helpers import (
create_insert_text_request,
create_delete_range_request,
create_format_text_request,
create_update_paragraph_style_request,
create_find_replace_request,
create_insert_table_request,
create_insert_page_break_request,
@@ -213,6 +214,59 @@ class BatchOperationManager:
description = f"format text {op['start_index']}-{op['end_index']} ({', '.join(format_changes)})"
elif op_type == "update_paragraph_style":
request = create_update_paragraph_style_request(
op["start_index"],
op["end_index"],
op.get("heading_level"),
op.get("alignment"),
op.get("line_spacing"),
op.get("indent_first_line"),
op.get("indent_start"),
op.get("indent_end"),
op.get("space_above"),
op.get("space_below"),
)
if not request:
raise ValueError("No paragraph style options provided")
_PT_PARAMS = {
"indent_first_line",
"indent_start",
"indent_end",
"space_above",
"space_below",
}
_SUFFIX = {
"heading_level": lambda v: f"H{v}",
"line_spacing": lambda v: f"{v}x",
}
style_changes = []
for param, name in [
("heading_level", "heading"),
("alignment", "alignment"),
("line_spacing", "line spacing"),
("indent_first_line", "first line indent"),
("indent_start", "start indent"),
("indent_end", "end indent"),
("space_above", "space above"),
("space_below", "space below"),
]:
if op.get(param) is not None:
raw = op[param]
fmt = _SUFFIX.get(param)
if fmt:
value = fmt(raw)
elif param in _PT_PARAMS:
value = f"{raw}pt"
else:
value = raw
style_changes.append(f"{name}: {value}")
description = f"paragraph style {op['start_index']}-{op['end_index']} ({', '.join(style_changes)})"
elif op_type == "insert_table":
request = create_insert_table_request(
op["index"], op["rows"], op["columns"]
@@ -235,6 +289,7 @@ class BatchOperationManager:
"delete_text",
"replace_text",
"format_text",
"update_paragraph_style",
"insert_table",
"insert_page_break",
"find_replace",
@@ -320,6 +375,20 @@ class BatchOperationManager:
],
"description": "Apply formatting to text range",
},
"update_paragraph_style": {
"required": ["start_index", "end_index"],
"optional": [
"heading_level",
"alignment",
"line_spacing",
"indent_first_line",
"indent_start",
"indent_end",
"space_above",
"space_below",
],
"description": "Apply paragraph-level styling (headings, alignment, spacing, indentation)",
},
"insert_table": {
"required": ["index", "rows", "columns"],
"description": "Insert table at specified index",
@@ -343,5 +412,12 @@ class BatchOperationManager:
"bold": True,
},
{"type": "insert_table", "index": 20, "rows": 2, "columns": 3},
{
"type": "update_paragraph_style",
"start_index": 1,
"end_index": 20,
"heading_level": 1,
"alignment": "CENTER",
},
],
}

View File

@@ -7,6 +7,7 @@ extracting validation patterns from individual tool functions.
import logging
from typing import Dict, Any, List, Tuple, Optional
from urllib.parse import urlparse
from gdocs.docs_helpers import validate_operation
@@ -38,6 +39,8 @@ class ValidationManager:
"valid_section_types": ["header", "footer"],
"valid_list_types": ["UNORDERED", "ORDERED"],
"valid_element_types": ["table", "list", "page_break"],
"valid_alignments": ["START", "CENTER", "END", "JUSTIFIED"],
"heading_level_range": (0, 6),
}
def validate_document_id(self, document_id: str) -> Tuple[bool, str]:
@@ -258,10 +261,112 @@ class ValidationManager:
if not link_url.strip():
return False, "link_url cannot be empty"
if not (link_url.startswith("http://") or link_url.startswith("https://")):
parsed = urlparse(link_url)
if parsed.scheme not in ("http", "https"):
return False, "link_url must start with http:// or https://"
if not parsed.netloc:
return False, "link_url must include a valid host"
return True, ""
def validate_paragraph_style_params(
self,
heading_level: Optional[int] = None,
alignment: Optional[str] = None,
line_spacing: Optional[float] = None,
indent_first_line: Optional[float] = None,
indent_start: Optional[float] = None,
indent_end: Optional[float] = None,
space_above: Optional[float] = None,
space_below: Optional[float] = None,
) -> Tuple[bool, str]:
"""
Validate paragraph style parameters.
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 (must be positive)
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
Returns:
Tuple of (is_valid, error_message)
"""
style_params = [
heading_level,
alignment,
line_spacing,
indent_first_line,
indent_start,
indent_end,
space_above,
space_below,
]
if all(param is None for param in style_params):
return (
False,
"At least one paragraph style parameter must be provided (heading_level, alignment, line_spacing, indent_first_line, indent_start, indent_end, space_above, or space_below)",
)
if heading_level is not None:
if not isinstance(heading_level, int):
return (
False,
f"heading_level must be an integer, got {type(heading_level).__name__}",
)
min_level, max_level = self.validation_rules["heading_level_range"]
if not (min_level <= heading_level <= max_level):
return (
False,
f"heading_level must be between {min_level} and {max_level}, got {heading_level}",
)
if alignment is not None:
if not isinstance(alignment, str):
return (
False,
f"alignment must be a string, got {type(alignment).__name__}",
)
valid = self.validation_rules["valid_alignments"]
if alignment.upper() not in valid:
return (
False,
f"alignment must be one of: {', '.join(valid)}, got '{alignment}'",
)
if line_spacing is not None:
if not isinstance(line_spacing, (int, float)):
return (
False,
f"line_spacing must be a number, got {type(line_spacing).__name__}",
)
if line_spacing <= 0:
return False, "line_spacing must be positive"
for param, name in [
(indent_first_line, "indent_first_line"),
(indent_start, "indent_start"),
(indent_end, "indent_end"),
(space_above, "space_above"),
(space_below, "space_below"),
]:
if param is not None:
if not isinstance(param, (int, float)):
return (
False,
f"{name} must be a number, got {type(param).__name__}",
)
# indent_first_line may be negative (hanging indent)
if name != "indent_first_line" and param < 0:
return False, f"{name} must be non-negative, got {param}"
return True, ""
def validate_color_param(
self, color: Optional[str], param_name: str
) -> Tuple[bool, str]:
@@ -512,6 +617,32 @@ class ValidationManager:
if not is_valid:
return False, f"Operation {i + 1} (format_text): {error_msg}"
elif op_type == "update_paragraph_style":
is_valid, error_msg = self.validate_paragraph_style_params(
op.get("heading_level"),
op.get("alignment"),
op.get("line_spacing"),
op.get("indent_first_line"),
op.get("indent_start"),
op.get("indent_end"),
op.get("space_above"),
op.get("space_below"),
)
if not is_valid:
return (
False,
f"Operation {i + 1} (update_paragraph_style): {error_msg}",
)
is_valid, error_msg = self.validate_index_range(
op["start_index"], op["end_index"]
)
if not is_valid:
return (
False,
f"Operation {i + 1} (update_paragraph_style): {error_msg}",
)
return True, ""
def validate_text_content(
@@ -547,7 +678,12 @@ class ValidationManager:
"constraints": self.validation_rules.copy(),
"supported_operations": {
"table_operations": ["create_table", "populate_table"],
"text_operations": ["insert_text", "format_text", "find_replace"],
"text_operations": [
"insert_text",
"format_text",
"find_replace",
"update_paragraph_style",
],
"element_operations": [
"insert_table",
"insert_list",