refactor tools to consolidate all modify actions
This commit is contained in:
@@ -729,405 +729,420 @@ async def format_sheet_range(
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("add_conditional_formatting", service_type="sheets")
|
||||
@handle_http_errors("manage_conditional_formatting", service_type="sheets")
|
||||
@require_google_service("sheets", "sheets_write")
|
||||
async def add_conditional_formatting(
|
||||
async def manage_conditional_formatting(
|
||||
service,
|
||||
user_google_email: str,
|
||||
spreadsheet_id: str,
|
||||
range_name: str,
|
||||
condition_type: str,
|
||||
condition_values: Optional[Union[str, List[Union[str, int, float]]]] = None,
|
||||
background_color: Optional[str] = None,
|
||||
text_color: Optional[str] = None,
|
||||
rule_index: Optional[int] = None,
|
||||
gradient_points: Optional[Union[str, List[dict]]] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Adds a conditional formatting rule to a range.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
||||
range_name (str): A1-style range (optionally with sheet name). Required.
|
||||
condition_type (str): Sheets condition type (e.g., NUMBER_GREATER, TEXT_CONTAINS, DATE_BEFORE, CUSTOM_FORMULA).
|
||||
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values for the condition; accepts a list or a JSON string representing a list. Depends on condition_type.
|
||||
background_color (Optional[str]): Hex background color to apply when condition matches.
|
||||
text_color (Optional[str]): Hex text color to apply when condition matches.
|
||||
rule_index (Optional[int]): Optional position to insert the rule (0-based) within the sheet's rules.
|
||||
gradient_points (Optional[Union[str, List[dict]]]): List (or JSON list) of gradient points for a color scale. If provided, a gradient rule is created and boolean parameters are ignored.
|
||||
|
||||
Returns:
|
||||
str: Confirmation of the added rule.
|
||||
"""
|
||||
logger.info(
|
||||
"[add_conditional_formatting] Invoked. Email: '%s', Spreadsheet: %s, Range: %s, Type: %s, Values: %s",
|
||||
user_google_email,
|
||||
spreadsheet_id,
|
||||
range_name,
|
||||
condition_type,
|
||||
condition_values,
|
||||
)
|
||||
|
||||
if rule_index is not None and (not isinstance(rule_index, int) or rule_index < 0):
|
||||
raise UserInputError("rule_index must be a non-negative integer when provided.")
|
||||
|
||||
condition_values_list = _parse_condition_values(condition_values)
|
||||
gradient_points_list = _parse_gradient_points(gradient_points)
|
||||
|
||||
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
|
||||
grid_range = _parse_a1_range(range_name, sheets)
|
||||
|
||||
target_sheet = None
|
||||
for sheet in sheets:
|
||||
if sheet.get("properties", {}).get("sheetId") == grid_range.get("sheetId"):
|
||||
target_sheet = sheet
|
||||
break
|
||||
if target_sheet is None:
|
||||
raise UserInputError(
|
||||
"Target sheet not found while adding conditional formatting."
|
||||
)
|
||||
|
||||
current_rules = target_sheet.get("conditionalFormats", []) or []
|
||||
|
||||
insert_at = rule_index if rule_index is not None else len(current_rules)
|
||||
if insert_at > len(current_rules):
|
||||
raise UserInputError(
|
||||
f"rule_index {insert_at} is out of range for sheet '{target_sheet.get('properties', {}).get('title', 'Unknown')}' "
|
||||
f"(current count: {len(current_rules)})."
|
||||
)
|
||||
|
||||
if gradient_points_list:
|
||||
new_rule = _build_gradient_rule([grid_range], gradient_points_list)
|
||||
rule_desc = "gradient"
|
||||
values_desc = ""
|
||||
applied_parts = [f"gradient points {len(gradient_points_list)}"]
|
||||
else:
|
||||
rule, cond_type_normalized = _build_boolean_rule(
|
||||
[grid_range],
|
||||
condition_type,
|
||||
condition_values_list,
|
||||
background_color,
|
||||
text_color,
|
||||
)
|
||||
new_rule = rule
|
||||
rule_desc = cond_type_normalized
|
||||
values_desc = ""
|
||||
if condition_values_list:
|
||||
values_desc = f" with values {condition_values_list}"
|
||||
applied_parts = []
|
||||
if background_color:
|
||||
applied_parts.append(f"background {background_color}")
|
||||
if text_color:
|
||||
applied_parts.append(f"text {text_color}")
|
||||
|
||||
new_rules_state = copy.deepcopy(current_rules)
|
||||
new_rules_state.insert(insert_at, new_rule)
|
||||
|
||||
add_rule_request = {"rule": new_rule}
|
||||
if rule_index is not None:
|
||||
add_rule_request["index"] = rule_index
|
||||
|
||||
request_body = {"requests": [{"addConditionalFormatRule": add_rule_request}]}
|
||||
|
||||
await asyncio.to_thread(
|
||||
service.spreadsheets()
|
||||
.batchUpdate(spreadsheetId=spreadsheet_id, body=request_body)
|
||||
.execute
|
||||
)
|
||||
|
||||
format_desc = ", ".join(applied_parts) if applied_parts else "format applied"
|
||||
|
||||
sheet_title = target_sheet.get("properties", {}).get("title", "Unknown")
|
||||
state_text = _format_conditional_rules_section(
|
||||
sheet_title, new_rules_state, sheet_titles, indent=""
|
||||
)
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
f"Added conditional format on '{range_name}' in spreadsheet {spreadsheet_id} "
|
||||
f"for {user_google_email}: {rule_desc}{values_desc}; format: {format_desc}.",
|
||||
state_text,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("update_conditional_formatting", service_type="sheets")
|
||||
@require_google_service("sheets", "sheets_write")
|
||||
async def update_conditional_formatting(
|
||||
service,
|
||||
user_google_email: str,
|
||||
spreadsheet_id: str,
|
||||
rule_index: int,
|
||||
action: str,
|
||||
range_name: Optional[str] = None,
|
||||
condition_type: Optional[str] = None,
|
||||
condition_values: Optional[Union[str, List[Union[str, int, float]]]] = None,
|
||||
background_color: Optional[str] = None,
|
||||
text_color: Optional[str] = None,
|
||||
sheet_name: Optional[str] = None,
|
||||
rule_index: Optional[int] = None,
|
||||
gradient_points: Optional[Union[str, List[dict]]] = None,
|
||||
sheet_name: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Updates an existing conditional formatting rule by index on a sheet.
|
||||
Manages conditional formatting rules on a Google Sheet. Supports adding,
|
||||
updating, and deleting conditional formatting rules via a single tool.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
||||
range_name (Optional[str]): A1-style range to apply the updated rule (optionally with sheet name). If omitted, existing ranges are preserved.
|
||||
rule_index (int): Index of the rule to update (0-based).
|
||||
condition_type (Optional[str]): Sheets condition type. If omitted, the existing rule's type is preserved.
|
||||
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values for the condition.
|
||||
background_color (Optional[str]): Hex background color when condition matches.
|
||||
text_color (Optional[str]): Hex text color when condition matches.
|
||||
sheet_name (Optional[str]): Sheet name to locate the rule when range_name is omitted. Defaults to first sheet.
|
||||
gradient_points (Optional[Union[str, List[dict]]]): If provided, updates the rule to a gradient color scale using these points.
|
||||
action (str): The operation to perform. Must be one of "add", "update",
|
||||
or "delete".
|
||||
range_name (Optional[str]): A1-style range (optionally with sheet name).
|
||||
Required for "add". Optional for "update" (preserves existing ranges
|
||||
if omitted). Not used for "delete".
|
||||
condition_type (Optional[str]): Sheets condition type (e.g., NUMBER_GREATER,
|
||||
TEXT_CONTAINS, DATE_BEFORE, CUSTOM_FORMULA). Required for "add".
|
||||
Optional for "update" (preserves existing type if omitted).
|
||||
condition_values (Optional[Union[str, List[Union[str, int, float]]]]): Values
|
||||
for the condition; accepts a list or a JSON string representing a list.
|
||||
Depends on condition_type. Used by "add" and "update".
|
||||
background_color (Optional[str]): Hex background color to apply when
|
||||
condition matches. Used by "add" and "update".
|
||||
text_color (Optional[str]): Hex text color to apply when condition matches.
|
||||
Used by "add" and "update".
|
||||
rule_index (Optional[int]): 0-based index of the rule. For "add", optionally
|
||||
specifies insertion position. Required for "update" and "delete".
|
||||
gradient_points (Optional[Union[str, List[dict]]]): List (or JSON list) of
|
||||
gradient points for a color scale. If provided, a gradient rule is created
|
||||
and boolean parameters are ignored. Used by "add" and "update".
|
||||
sheet_name (Optional[str]): Sheet name to locate the rule when range_name is
|
||||
omitted. Defaults to the first sheet. Used by "update" and "delete".
|
||||
|
||||
Returns:
|
||||
str: Confirmation of the updated rule and the current rule state.
|
||||
str: Confirmation of the operation and the current rule state.
|
||||
"""
|
||||
allowed_actions = {"add", "update", "delete"}
|
||||
action_normalized = action.strip().lower()
|
||||
if action_normalized not in allowed_actions:
|
||||
raise UserInputError(
|
||||
f"action must be one of {sorted(allowed_actions)}, got '{action}'."
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[update_conditional_formatting] Invoked. Email: '%s', Spreadsheet: %s, Range: %s, Rule Index: %s",
|
||||
"[manage_conditional_formatting] Invoked. Action: '%s', Email: '%s', Spreadsheet: %s",
|
||||
action_normalized,
|
||||
user_google_email,
|
||||
spreadsheet_id,
|
||||
range_name,
|
||||
rule_index,
|
||||
)
|
||||
|
||||
if not isinstance(rule_index, int) or rule_index < 0:
|
||||
raise UserInputError("rule_index must be a non-negative integer.")
|
||||
if action_normalized == "add":
|
||||
if not range_name:
|
||||
raise UserInputError("range_name is required for action 'add'.")
|
||||
if not condition_type and not gradient_points:
|
||||
raise UserInputError(
|
||||
"condition_type (or gradient_points) is required for action 'add'."
|
||||
)
|
||||
|
||||
condition_values_list = _parse_condition_values(condition_values)
|
||||
gradient_points_list = _parse_gradient_points(gradient_points)
|
||||
if rule_index is not None and (
|
||||
not isinstance(rule_index, int) or rule_index < 0
|
||||
):
|
||||
raise UserInputError(
|
||||
"rule_index must be a non-negative integer when provided."
|
||||
)
|
||||
|
||||
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
|
||||
condition_values_list = _parse_condition_values(condition_values)
|
||||
gradient_points_list = _parse_gradient_points(gradient_points)
|
||||
|
||||
target_sheet = None
|
||||
grid_range = None
|
||||
if range_name:
|
||||
sheets, sheet_titles = await _fetch_sheets_with_rules(
|
||||
service, spreadsheet_id
|
||||
)
|
||||
grid_range = _parse_a1_range(range_name, sheets)
|
||||
|
||||
target_sheet = None
|
||||
for sheet in sheets:
|
||||
if sheet.get("properties", {}).get("sheetId") == grid_range.get("sheetId"):
|
||||
if (
|
||||
sheet.get("properties", {}).get("sheetId")
|
||||
== grid_range.get("sheetId")
|
||||
):
|
||||
target_sheet = sheet
|
||||
break
|
||||
else:
|
||||
if target_sheet is None:
|
||||
raise UserInputError(
|
||||
"Target sheet not found while adding conditional formatting."
|
||||
)
|
||||
|
||||
current_rules = target_sheet.get("conditionalFormats", []) or []
|
||||
|
||||
insert_at = rule_index if rule_index is not None else len(current_rules)
|
||||
if insert_at > len(current_rules):
|
||||
raise UserInputError(
|
||||
f"rule_index {insert_at} is out of range for sheet "
|
||||
f"'{target_sheet.get('properties', {}).get('title', 'Unknown')}' "
|
||||
f"(current count: {len(current_rules)})."
|
||||
)
|
||||
|
||||
if gradient_points_list:
|
||||
new_rule = _build_gradient_rule([grid_range], gradient_points_list)
|
||||
rule_desc = "gradient"
|
||||
values_desc = ""
|
||||
applied_parts = [f"gradient points {len(gradient_points_list)}"]
|
||||
else:
|
||||
rule, cond_type_normalized = _build_boolean_rule(
|
||||
[grid_range],
|
||||
condition_type,
|
||||
condition_values_list,
|
||||
background_color,
|
||||
text_color,
|
||||
)
|
||||
new_rule = rule
|
||||
rule_desc = cond_type_normalized
|
||||
values_desc = ""
|
||||
if condition_values_list:
|
||||
values_desc = f" with values {condition_values_list}"
|
||||
applied_parts = []
|
||||
if background_color:
|
||||
applied_parts.append(f"background {background_color}")
|
||||
if text_color:
|
||||
applied_parts.append(f"text {text_color}")
|
||||
|
||||
new_rules_state = copy.deepcopy(current_rules)
|
||||
new_rules_state.insert(insert_at, new_rule)
|
||||
|
||||
add_rule_request = {"rule": new_rule}
|
||||
if rule_index is not None:
|
||||
add_rule_request["index"] = rule_index
|
||||
|
||||
request_body = {
|
||||
"requests": [{"addConditionalFormatRule": add_rule_request}]
|
||||
}
|
||||
|
||||
await asyncio.to_thread(
|
||||
service.spreadsheets()
|
||||
.batchUpdate(spreadsheetId=spreadsheet_id, body=request_body)
|
||||
.execute
|
||||
)
|
||||
|
||||
format_desc = (
|
||||
", ".join(applied_parts) if applied_parts else "format applied"
|
||||
)
|
||||
|
||||
sheet_title = target_sheet.get("properties", {}).get("title", "Unknown")
|
||||
state_text = _format_conditional_rules_section(
|
||||
sheet_title, new_rules_state, sheet_titles, indent=""
|
||||
)
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
f"Added conditional format on '{range_name}' in spreadsheet "
|
||||
f"{spreadsheet_id} for {user_google_email}: "
|
||||
f"{rule_desc}{values_desc}; format: {format_desc}.",
|
||||
state_text,
|
||||
]
|
||||
)
|
||||
|
||||
elif action_normalized == "update":
|
||||
if rule_index is None:
|
||||
raise UserInputError("rule_index is required for action 'update'.")
|
||||
if not isinstance(rule_index, int) or rule_index < 0:
|
||||
raise UserInputError("rule_index must be a non-negative integer.")
|
||||
|
||||
condition_values_list = _parse_condition_values(condition_values)
|
||||
gradient_points_list = _parse_gradient_points(gradient_points)
|
||||
|
||||
sheets, sheet_titles = await _fetch_sheets_with_rules(
|
||||
service, spreadsheet_id
|
||||
)
|
||||
|
||||
target_sheet = None
|
||||
grid_range = None
|
||||
if range_name:
|
||||
grid_range = _parse_a1_range(range_name, sheets)
|
||||
for sheet in sheets:
|
||||
if (
|
||||
sheet.get("properties", {}).get("sheetId")
|
||||
== grid_range.get("sheetId")
|
||||
):
|
||||
target_sheet = sheet
|
||||
break
|
||||
else:
|
||||
target_sheet = _select_sheet(sheets, sheet_name)
|
||||
|
||||
if target_sheet is None:
|
||||
raise UserInputError(
|
||||
"Target sheet not found while updating conditional formatting."
|
||||
)
|
||||
|
||||
sheet_props = target_sheet.get("properties", {})
|
||||
sheet_id = sheet_props.get("sheetId")
|
||||
sheet_title = sheet_props.get("title", f"Sheet {sheet_id}")
|
||||
|
||||
rules = target_sheet.get("conditionalFormats", []) or []
|
||||
if rule_index >= len(rules):
|
||||
raise UserInputError(
|
||||
f"rule_index {rule_index} is out of range for sheet "
|
||||
f"'{sheet_title}' (current count: {len(rules)})."
|
||||
)
|
||||
|
||||
existing_rule = rules[rule_index]
|
||||
ranges_to_use = existing_rule.get("ranges", [])
|
||||
if range_name:
|
||||
ranges_to_use = [grid_range]
|
||||
if not ranges_to_use:
|
||||
ranges_to_use = [{"sheetId": sheet_id}]
|
||||
|
||||
new_rule = None
|
||||
rule_desc = ""
|
||||
values_desc = ""
|
||||
format_desc = ""
|
||||
|
||||
if gradient_points_list is not None:
|
||||
new_rule = _build_gradient_rule(ranges_to_use, gradient_points_list)
|
||||
rule_desc = "gradient"
|
||||
format_desc = f"gradient points {len(gradient_points_list)}"
|
||||
elif "gradientRule" in existing_rule:
|
||||
if any(
|
||||
[
|
||||
background_color,
|
||||
text_color,
|
||||
condition_type,
|
||||
condition_values_list,
|
||||
]
|
||||
):
|
||||
raise UserInputError(
|
||||
"Existing rule is a gradient rule. Provide gradient_points "
|
||||
"to update it, or omit formatting/condition parameters to "
|
||||
"keep it unchanged."
|
||||
)
|
||||
new_rule = {
|
||||
"ranges": ranges_to_use,
|
||||
"gradientRule": existing_rule.get("gradientRule", {}),
|
||||
}
|
||||
rule_desc = "gradient"
|
||||
format_desc = "gradient (unchanged)"
|
||||
else:
|
||||
existing_boolean = existing_rule.get("booleanRule", {})
|
||||
existing_condition = existing_boolean.get("condition", {})
|
||||
existing_format = copy.deepcopy(
|
||||
existing_boolean.get("format", {})
|
||||
)
|
||||
|
||||
cond_type = (
|
||||
condition_type or existing_condition.get("type", "")
|
||||
).upper()
|
||||
if not cond_type:
|
||||
raise UserInputError(
|
||||
"condition_type is required for boolean rules."
|
||||
)
|
||||
if cond_type not in CONDITION_TYPES:
|
||||
raise UserInputError(
|
||||
f"condition_type must be one of {sorted(CONDITION_TYPES)}."
|
||||
)
|
||||
|
||||
if condition_values_list is not None:
|
||||
cond_values = [
|
||||
{"userEnteredValue": str(val)}
|
||||
for val in condition_values_list
|
||||
]
|
||||
else:
|
||||
cond_values = existing_condition.get("values")
|
||||
|
||||
new_format = (
|
||||
copy.deepcopy(existing_format) if existing_format else {}
|
||||
)
|
||||
if background_color is not None:
|
||||
bg_color_parsed = _parse_hex_color(background_color)
|
||||
if bg_color_parsed:
|
||||
new_format["backgroundColor"] = bg_color_parsed
|
||||
elif "backgroundColor" in new_format:
|
||||
del new_format["backgroundColor"]
|
||||
if text_color is not None:
|
||||
text_color_parsed = _parse_hex_color(text_color)
|
||||
text_format = copy.deepcopy(
|
||||
new_format.get("textFormat", {})
|
||||
)
|
||||
if text_color_parsed:
|
||||
text_format["foregroundColor"] = text_color_parsed
|
||||
elif "foregroundColor" in text_format:
|
||||
del text_format["foregroundColor"]
|
||||
if text_format:
|
||||
new_format["textFormat"] = text_format
|
||||
elif "textFormat" in new_format:
|
||||
del new_format["textFormat"]
|
||||
|
||||
if not new_format:
|
||||
raise UserInputError(
|
||||
"At least one format option must remain on the rule."
|
||||
)
|
||||
|
||||
new_rule = {
|
||||
"ranges": ranges_to_use,
|
||||
"booleanRule": {
|
||||
"condition": {"type": cond_type},
|
||||
"format": new_format,
|
||||
},
|
||||
}
|
||||
if cond_values:
|
||||
new_rule["booleanRule"]["condition"]["values"] = cond_values
|
||||
|
||||
rule_desc = cond_type
|
||||
if condition_values_list:
|
||||
values_desc = f" with values {condition_values_list}"
|
||||
format_parts = []
|
||||
if "backgroundColor" in new_format:
|
||||
format_parts.append("background updated")
|
||||
if "textFormat" in new_format and new_format["textFormat"].get(
|
||||
"foregroundColor"
|
||||
):
|
||||
format_parts.append("text color updated")
|
||||
format_desc = (
|
||||
", ".join(format_parts) if format_parts else "format preserved"
|
||||
)
|
||||
|
||||
new_rules_state = copy.deepcopy(rules)
|
||||
new_rules_state[rule_index] = new_rule
|
||||
|
||||
request_body = {
|
||||
"requests": [
|
||||
{
|
||||
"updateConditionalFormatRule": {
|
||||
"index": rule_index,
|
||||
"sheetId": sheet_id,
|
||||
"rule": new_rule,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
await asyncio.to_thread(
|
||||
service.spreadsheets()
|
||||
.batchUpdate(spreadsheetId=spreadsheet_id, body=request_body)
|
||||
.execute
|
||||
)
|
||||
|
||||
state_text = _format_conditional_rules_section(
|
||||
sheet_title, new_rules_state, sheet_titles, indent=""
|
||||
)
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
f"Updated conditional format at index {rule_index} on sheet "
|
||||
f"'{sheet_title}' in spreadsheet {spreadsheet_id} "
|
||||
f"for {user_google_email}: "
|
||||
f"{rule_desc}{values_desc}; format: {format_desc}.",
|
||||
state_text,
|
||||
]
|
||||
)
|
||||
|
||||
else: # action_normalized == "delete"
|
||||
if rule_index is None:
|
||||
raise UserInputError("rule_index is required for action 'delete'.")
|
||||
if not isinstance(rule_index, int) or rule_index < 0:
|
||||
raise UserInputError("rule_index must be a non-negative integer.")
|
||||
|
||||
sheets, sheet_titles = await _fetch_sheets_with_rules(
|
||||
service, spreadsheet_id
|
||||
)
|
||||
target_sheet = _select_sheet(sheets, sheet_name)
|
||||
|
||||
if target_sheet is None:
|
||||
raise UserInputError(
|
||||
"Target sheet not found while updating conditional formatting."
|
||||
)
|
||||
|
||||
sheet_props = target_sheet.get("properties", {})
|
||||
sheet_id = sheet_props.get("sheetId")
|
||||
sheet_title = sheet_props.get("title", f"Sheet {sheet_id}")
|
||||
|
||||
rules = target_sheet.get("conditionalFormats", []) or []
|
||||
if rule_index >= len(rules):
|
||||
raise UserInputError(
|
||||
f"rule_index {rule_index} is out of range for sheet '{sheet_title}' (current count: {len(rules)})."
|
||||
)
|
||||
|
||||
existing_rule = rules[rule_index]
|
||||
ranges_to_use = existing_rule.get("ranges", [])
|
||||
if range_name:
|
||||
ranges_to_use = [grid_range]
|
||||
if not ranges_to_use:
|
||||
ranges_to_use = [{"sheetId": sheet_id}]
|
||||
|
||||
new_rule = None
|
||||
rule_desc = ""
|
||||
values_desc = ""
|
||||
format_desc = ""
|
||||
|
||||
if gradient_points_list is not None:
|
||||
new_rule = _build_gradient_rule(ranges_to_use, gradient_points_list)
|
||||
rule_desc = "gradient"
|
||||
format_desc = f"gradient points {len(gradient_points_list)}"
|
||||
elif "gradientRule" in existing_rule:
|
||||
if any([background_color, text_color, condition_type, condition_values_list]):
|
||||
sheet_props = target_sheet.get("properties", {})
|
||||
sheet_id = sheet_props.get("sheetId")
|
||||
target_sheet_name = sheet_props.get("title", f"Sheet {sheet_id}")
|
||||
rules = target_sheet.get("conditionalFormats", []) or []
|
||||
if rule_index >= len(rules):
|
||||
raise UserInputError(
|
||||
"Existing rule is a gradient rule. Provide gradient_points to update it, or omit formatting/condition parameters to keep it unchanged."
|
||||
)
|
||||
new_rule = {
|
||||
"ranges": ranges_to_use,
|
||||
"gradientRule": existing_rule.get("gradientRule", {}),
|
||||
}
|
||||
rule_desc = "gradient"
|
||||
format_desc = "gradient (unchanged)"
|
||||
else:
|
||||
existing_boolean = existing_rule.get("booleanRule", {})
|
||||
existing_condition = existing_boolean.get("condition", {})
|
||||
existing_format = copy.deepcopy(existing_boolean.get("format", {}))
|
||||
|
||||
cond_type = (condition_type or existing_condition.get("type", "")).upper()
|
||||
if not cond_type:
|
||||
raise UserInputError("condition_type is required for boolean rules.")
|
||||
if cond_type not in CONDITION_TYPES:
|
||||
raise UserInputError(
|
||||
f"condition_type must be one of {sorted(CONDITION_TYPES)}."
|
||||
f"rule_index {rule_index} is out of range for sheet "
|
||||
f"'{target_sheet_name}' (current count: {len(rules)})."
|
||||
)
|
||||
|
||||
if condition_values_list is not None:
|
||||
cond_values = [
|
||||
{"userEnteredValue": str(val)} for val in condition_values_list
|
||||
new_rules_state = copy.deepcopy(rules)
|
||||
del new_rules_state[rule_index]
|
||||
|
||||
request_body = {
|
||||
"requests": [
|
||||
{
|
||||
"deleteConditionalFormatRule": {
|
||||
"index": rule_index,
|
||||
"sheetId": sheet_id,
|
||||
}
|
||||
}
|
||||
]
|
||||
else:
|
||||
cond_values = existing_condition.get("values")
|
||||
|
||||
new_format = copy.deepcopy(existing_format) if existing_format else {}
|
||||
if background_color is not None:
|
||||
bg_color_parsed = _parse_hex_color(background_color)
|
||||
if bg_color_parsed:
|
||||
new_format["backgroundColor"] = bg_color_parsed
|
||||
elif "backgroundColor" in new_format:
|
||||
del new_format["backgroundColor"]
|
||||
if text_color is not None:
|
||||
text_color_parsed = _parse_hex_color(text_color)
|
||||
text_format = copy.deepcopy(new_format.get("textFormat", {}))
|
||||
if text_color_parsed:
|
||||
text_format["foregroundColor"] = text_color_parsed
|
||||
elif "foregroundColor" in text_format:
|
||||
del text_format["foregroundColor"]
|
||||
if text_format:
|
||||
new_format["textFormat"] = text_format
|
||||
elif "textFormat" in new_format:
|
||||
del new_format["textFormat"]
|
||||
|
||||
if not new_format:
|
||||
raise UserInputError("At least one format option must remain on the rule.")
|
||||
|
||||
new_rule = {
|
||||
"ranges": ranges_to_use,
|
||||
"booleanRule": {
|
||||
"condition": {"type": cond_type},
|
||||
"format": new_format,
|
||||
},
|
||||
}
|
||||
if cond_values:
|
||||
new_rule["booleanRule"]["condition"]["values"] = cond_values
|
||||
|
||||
rule_desc = cond_type
|
||||
if condition_values_list:
|
||||
values_desc = f" with values {condition_values_list}"
|
||||
format_parts = []
|
||||
if "backgroundColor" in new_format:
|
||||
format_parts.append("background updated")
|
||||
if "textFormat" in new_format and new_format["textFormat"].get(
|
||||
"foregroundColor"
|
||||
):
|
||||
format_parts.append("text color updated")
|
||||
format_desc = ", ".join(format_parts) if format_parts else "format preserved"
|
||||
|
||||
new_rules_state = copy.deepcopy(rules)
|
||||
new_rules_state[rule_index] = new_rule
|
||||
|
||||
request_body = {
|
||||
"requests": [
|
||||
{
|
||||
"updateConditionalFormatRule": {
|
||||
"index": rule_index,
|
||||
"sheetId": sheet_id,
|
||||
"rule": new_rule,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
await asyncio.to_thread(
|
||||
service.spreadsheets()
|
||||
.batchUpdate(spreadsheetId=spreadsheet_id, body=request_body)
|
||||
.execute
|
||||
)
|
||||
|
||||
state_text = _format_conditional_rules_section(
|
||||
sheet_title, new_rules_state, sheet_titles, indent=""
|
||||
)
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
f"Updated conditional format at index {rule_index} on sheet '{sheet_title}' in spreadsheet {spreadsheet_id} "
|
||||
f"for {user_google_email}: {rule_desc}{values_desc}; format: {format_desc}.",
|
||||
state_text,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@server.tool()
|
||||
@handle_http_errors("delete_conditional_formatting", service_type="sheets")
|
||||
@require_google_service("sheets", "sheets_write")
|
||||
async def delete_conditional_formatting(
|
||||
service,
|
||||
user_google_email: str,
|
||||
spreadsheet_id: str,
|
||||
rule_index: int,
|
||||
sheet_name: Optional[str] = None,
|
||||
) -> str:
|
||||
"""
|
||||
Deletes an existing conditional formatting rule by index on a sheet.
|
||||
|
||||
Args:
|
||||
user_google_email (str): The user's Google email address. Required.
|
||||
spreadsheet_id (str): The ID of the spreadsheet. Required.
|
||||
rule_index (int): Index of the rule to delete (0-based).
|
||||
sheet_name (Optional[str]): Name of the sheet that contains the rule. Defaults to the first sheet if not provided.
|
||||
|
||||
Returns:
|
||||
str: Confirmation of the deletion and the current rule state.
|
||||
"""
|
||||
logger.info(
|
||||
"[delete_conditional_formatting] Invoked. Email: '%s', Spreadsheet: %s, Sheet: %s, Rule Index: %s",
|
||||
user_google_email,
|
||||
spreadsheet_id,
|
||||
sheet_name,
|
||||
rule_index,
|
||||
)
|
||||
|
||||
if not isinstance(rule_index, int) or rule_index < 0:
|
||||
raise UserInputError("rule_index must be a non-negative integer.")
|
||||
|
||||
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
|
||||
target_sheet = _select_sheet(sheets, sheet_name)
|
||||
|
||||
sheet_props = target_sheet.get("properties", {})
|
||||
sheet_id = sheet_props.get("sheetId")
|
||||
target_sheet_name = sheet_props.get("title", f"Sheet {sheet_id}")
|
||||
rules = target_sheet.get("conditionalFormats", []) or []
|
||||
if rule_index >= len(rules):
|
||||
raise UserInputError(
|
||||
f"rule_index {rule_index} is out of range for sheet '{target_sheet_name}' (current count: {len(rules)})."
|
||||
await asyncio.to_thread(
|
||||
service.spreadsheets()
|
||||
.batchUpdate(spreadsheetId=spreadsheet_id, body=request_body)
|
||||
.execute
|
||||
)
|
||||
|
||||
new_rules_state = copy.deepcopy(rules)
|
||||
del new_rules_state[rule_index]
|
||||
state_text = _format_conditional_rules_section(
|
||||
target_sheet_name, new_rules_state, sheet_titles, indent=""
|
||||
)
|
||||
|
||||
request_body = {
|
||||
"requests": [
|
||||
{
|
||||
"deleteConditionalFormatRule": {
|
||||
"index": rule_index,
|
||||
"sheetId": sheet_id,
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
await asyncio.to_thread(
|
||||
service.spreadsheets()
|
||||
.batchUpdate(spreadsheetId=spreadsheet_id, body=request_body)
|
||||
.execute
|
||||
)
|
||||
|
||||
state_text = _format_conditional_rules_section(
|
||||
target_sheet_name, new_rules_state, sheet_titles, indent=""
|
||||
)
|
||||
|
||||
return "\n".join(
|
||||
[
|
||||
f"Deleted conditional format at index {rule_index} on sheet '{target_sheet_name}' in spreadsheet {spreadsheet_id} for {user_google_email}.",
|
||||
state_text,
|
||||
]
|
||||
)
|
||||
return "\n".join(
|
||||
[
|
||||
f"Deleted conditional format at index {rule_index} on sheet "
|
||||
f"'{target_sheet_name}' in spreadsheet {spreadsheet_id} "
|
||||
f"for {user_google_email}.",
|
||||
state_text,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@server.tool()
|
||||
|
||||
Reference in New Issue
Block a user