This commit is contained in:
Taylor Wilsdon
2026-03-01 13:26:14 -05:00
9 changed files with 105 additions and 130 deletions

View File

@@ -17,8 +17,12 @@ logger = logging.getLogger(__name__)
async def _manage_comment_dispatch(
service, app_name: str, file_id: str, action: str,
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
service,
app_name: str,
file_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
) -> str:
"""Route comment management actions to the appropriate implementation."""
action_lower = action.lower().strip()
@@ -28,14 +32,20 @@ async def _manage_comment_dispatch(
return await _create_comment_impl(service, app_name, file_id, comment_content)
elif action_lower == "reply":
if not comment_id or not comment_content:
raise ValueError("comment_id and comment_content are required for reply action")
return await _reply_to_comment_impl(service, app_name, file_id, comment_id, comment_content)
raise ValueError(
"comment_id and comment_content are required for reply action"
)
return await _reply_to_comment_impl(
service, app_name, file_id, comment_id, comment_content
)
elif action_lower == "resolve":
if not comment_id:
raise ValueError("comment_id is required for resolve action")
return await _resolve_comment_impl(service, app_name, file_id, comment_id)
else:
raise ValueError(f"Invalid action '{action_lower}'. Must be 'create', 'reply', or 'resolve'.")
raise ValueError(
f"Invalid action '{action_lower}'. Must be 'create', 'reply', or 'resolve'."
)
def create_comment_tools(app_name: str, file_id_param: str):
@@ -67,8 +77,12 @@ def create_comment_tools(app_name: str, file_id_param: str):
@require_google_service("drive", "drive_file")
@handle_http_errors(manage_func_name, service_type="drive")
async def manage_comment(
service, user_google_email: str, document_id: str, action: str,
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
service,
user_google_email: str,
document_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
) -> str:
"""Manage comments on a Google Document.
@@ -94,8 +108,12 @@ def create_comment_tools(app_name: str, file_id_param: str):
@require_google_service("drive", "drive_file")
@handle_http_errors(manage_func_name, service_type="drive")
async def manage_comment(
service, user_google_email: str, spreadsheet_id: str, action: str,
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
service,
user_google_email: str,
spreadsheet_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
) -> str:
"""Manage comments on a Google Spreadsheet.
@@ -121,8 +139,12 @@ def create_comment_tools(app_name: str, file_id_param: str):
@require_google_service("drive", "drive_file")
@handle_http_errors(manage_func_name, service_type="drive")
async def manage_comment(
service, user_google_email: str, presentation_id: str, action: str,
comment_content: Optional[str] = None, comment_id: Optional[str] = None,
service,
user_google_email: str,
presentation_id: str,
action: str,
comment_content: Optional[str] = None,
comment_id: Optional[str] = None,
) -> str:
"""Manage comments on a Google Presentation.

View File

@@ -512,9 +512,9 @@ async def manage_deployment(
service, user_google_email, script_id, deployment_id
)
else:
raise ValueError(f"Invalid action '{action}'. Must be 'create', 'update', or 'delete'.")
raise ValueError(
f"Invalid action '{action}'. Must be 'create', 'update', or 'delete'."
)
async def _list_deployments_impl(
@@ -604,8 +604,6 @@ async def _update_deployment_impl(
return "\n".join(output)
async def _delete_deployment_impl(
service: Any,
user_google_email: str,
@@ -630,8 +628,6 @@ async def _delete_deployment_impl(
return output
async def _list_script_processes_impl(
service: Any,
user_google_email: str,

View File

@@ -534,7 +534,6 @@ async def get_events(
return text_output
# ---------------------------------------------------------------------------
# Internal implementation functions for event create/modify/delete.
# These are called by both the consolidated ``manage_event`` tool and the
@@ -789,7 +788,6 @@ def _normalize_attendees(
return normalized if normalized else None
async def _modify_event_impl(
service,
user_google_email: str,
@@ -1139,7 +1137,9 @@ async def manage_event(
action_lower = action.lower().strip()
if action_lower == "create":
if not summary or not start_time or not end_time:
raise ValueError("summary, start_time, and end_time are required for create action")
raise ValueError(
"summary, start_time, and end_time are required for create action"
)
return await _create_event_impl(
service=service,
user_google_email=user_google_email,
@@ -1154,7 +1154,9 @@ async def manage_event(
attachments=attachments,
add_google_meet=add_google_meet or False,
reminders=reminders,
use_default_reminders=use_default_reminders if use_default_reminders is not None else True,
use_default_reminders=use_default_reminders
if use_default_reminders is not None
else True,
transparency=transparency,
visibility=visibility,
guests_can_modify=guests_can_modify,
@@ -1196,7 +1198,9 @@ async def manage_event(
calendar_id=calendar_id,
)
else:
raise ValueError(f"Invalid action '{action_lower}'. Must be 'create', 'update', or 'delete'.")
raise ValueError(
f"Invalid action '{action_lower}'. Must be 'create', 'update', or 'delete'."
)
# ---------------------------------------------------------------------------
@@ -1204,12 +1208,6 @@ async def manage_event(
# ---------------------------------------------------------------------------
@server.tool()
@handle_http_errors("query_freebusy", is_read_only=True, service_type="calendar")
@require_google_service("calendar", "calendar_read")

View File

@@ -1067,7 +1067,9 @@ async def manage_contact_group(
.execute
)
response = f"Contact group {group_id} has been deleted for {user_google_email}."
response = (
f"Contact group {group_id} has been deleted for {user_google_email}."
)
if delete_contacts:
response += " Contacts in the group were also deleted."
else:
@@ -1146,4 +1148,3 @@ async def manage_contact_group(
message = f"Unexpected error: {e}."
logger.exception(message)
raise Exception(message)

View File

@@ -1854,9 +1854,7 @@ async def manage_drive_access(
validate_share_type(share_type)
if share_type in ("user", "group") and not share_with:
raise ValueError(
f"share_with is required for share_type '{share_type}'"
)
raise ValueError(f"share_with is required for share_type '{share_type}'")
if share_type == "domain" and not share_with:
raise ValueError(
"share_with (domain name) is required for share_type 'domain'"
@@ -1898,14 +1896,16 @@ async def manage_drive_access(
service.permissions().create(**create_params).execute
)
return "\n".join([
f"Successfully shared '{file_metadata.get('name', 'Unknown')}'",
"",
"Permission created:",
f" - {format_permission_info(created_permission)}",
"",
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
])
return "\n".join(
[
f"Successfully shared '{file_metadata.get('name', 'Unknown')}'",
"",
"Permission created:",
f" - {format_permission_info(created_permission)}",
"",
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
]
)
# --- grant_batch: share with multiple recipients ---
if action == "grant_batch":
@@ -2001,10 +2001,12 @@ async def manage_drive_access(
"Results:",
]
output_parts.extend(results)
output_parts.extend([
"",
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
])
output_parts.extend(
[
"",
f"View link: {file_metadata.get('webViewLink', 'N/A')}",
]
)
return "\n".join(output_parts)
# --- update: modify an existing permission ---
@@ -2056,12 +2058,14 @@ async def manage_drive_access(
.execute
)
return "\n".join([
f"Successfully updated permission on '{file_metadata.get('name', 'Unknown')}'",
"",
"Updated permission:",
f" - {format_permission_info(updated_permission)}",
])
return "\n".join(
[
f"Successfully updated permission on '{file_metadata.get('name', 'Unknown')}'",
"",
"Updated permission:",
f" - {format_permission_info(updated_permission)}",
]
)
# --- revoke: remove an existing permission ---
if action == "revoke":
@@ -2083,11 +2087,13 @@ async def manage_drive_access(
.execute
)
return "\n".join([
f"Successfully removed permission from '{file_metadata.get('name', 'Unknown')}'",
"",
f"Permission ID '{permission_id}' has been revoked.",
])
return "\n".join(
[
f"Successfully removed permission from '{file_metadata.get('name', 'Unknown')}'",
"",
f"Permission ID '{permission_id}' has been revoked.",
]
)
# --- transfer_owner: transfer file ownership ---
# action == "transfer_owner"
@@ -2134,14 +2140,6 @@ async def manage_drive_access(
return "\n".join(output_parts)
@server.tool()
@handle_http_errors("copy_drive_file", is_read_only=False, service_type="drive")
@require_google_service("drive", "drive_file")
@@ -2214,8 +2212,6 @@ async def copy_drive_file(
return "\n".join(output_parts)
@server.tool()
@handle_http_errors(
"set_drive_file_permissions", is_read_only=False, service_type="drive"

View File

@@ -1951,7 +1951,9 @@ async def manage_gmail_filter(
action_lower = action.lower().strip()
if action_lower == "create":
if not criteria or not filter_action:
raise ValueError("criteria and filter_action are required for create action")
raise ValueError(
"criteria and filter_action are required for create action"
)
logger.info("[manage_gmail_filter] Creating filter")
filter_body = {"criteria": criteria, "action": filter_action}
created_filter = await asyncio.to_thread(
@@ -1971,7 +1973,11 @@ async def manage_gmail_filter(
service.users().settings().filters().get(userId="me", id=filter_id).execute
)
await asyncio.to_thread(
service.users().settings().filters().delete(userId="me", id=filter_id).execute
service.users()
.settings()
.filters()
.delete(userId="me", id=filter_id)
.execute
)
criteria_info = filter_details.get("criteria", {})
action_info = filter_details.get("action", {})
@@ -1982,11 +1988,9 @@ async def manage_gmail_filter(
f"Action: {action_info or '(none)'}"
)
else:
raise ValueError(f"Invalid action '{action_lower}'. Must be 'create' or 'delete'.")
raise ValueError(
f"Invalid action '{action_lower}'. Must be 'create' or 'delete'."
)
@server.tool()

View File

@@ -232,5 +232,3 @@ async def get_search_engine_info(service, user_google_email: str) -> str:
logger.info(f"Search engine info retrieved successfully for {user_google_email}")
return confirmation_message

View File

@@ -814,17 +814,12 @@ async def manage_conditional_formatting(
else _parse_condition_values(condition_values)
)
sheets, sheet_titles = await _fetch_sheets_with_rules(
service, spreadsheet_id
)
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
if target_sheet is None:
@@ -873,9 +868,7 @@ async def manage_conditional_formatting(
if rule_index is not None:
add_rule_request["index"] = rule_index
request_body = {
"requests": [{"addConditionalFormatRule": add_rule_request}]
}
request_body = {"requests": [{"addConditionalFormatRule": add_rule_request}]}
await asyncio.to_thread(
service.spreadsheets()
@@ -883,9 +876,7 @@ async def manage_conditional_formatting(
.execute
)
format_desc = (
", ".join(applied_parts) if applied_parts else "format applied"
)
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(
@@ -914,18 +905,15 @@ async def manage_conditional_formatting(
else _parse_condition_values(condition_values)
)
sheets, sheet_titles = await _fetch_sheets_with_rules(
service, spreadsheet_id
)
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")
if sheet.get("properties", {}).get("sheetId") == grid_range.get(
"sheetId"
):
target_sheet = sheet
break
@@ -987,17 +975,11 @@ async def manage_conditional_formatting(
else:
existing_boolean = existing_rule.get("booleanRule", {})
existing_condition = existing_boolean.get("condition", {})
existing_format = copy.deepcopy(
existing_boolean.get("format", {})
)
existing_format = copy.deepcopy(existing_boolean.get("format", {}))
cond_type = (
condition_type or existing_condition.get("type", "")
).upper()
cond_type = (condition_type or existing_condition.get("type", "")).upper()
if not cond_type:
raise UserInputError(
"condition_type is required for boolean rules."
)
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)}."
@@ -1005,15 +987,12 @@ async def manage_conditional_formatting(
if condition_values_list is not None:
cond_values = [
{"userEnteredValue": str(val)}
for val in condition_values_list
{"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 {}
)
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:
@@ -1022,9 +1001,7 @@ async def manage_conditional_formatting(
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", {})
)
text_format = copy.deepcopy(new_format.get("textFormat", {}))
if text_color_parsed:
text_format["foregroundColor"] = text_color_parsed
elif "foregroundColor" in text_format:
@@ -1104,9 +1081,7 @@ async def manage_conditional_formatting(
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
)
sheets, sheet_titles = await _fetch_sheets_with_rules(service, spreadsheet_id)
target_sheet = _select_sheet(sheets, sheet_name)
sheet_props = target_sheet.get("properties", {})

View File

@@ -386,12 +386,6 @@ async def manage_task_list(
# --- Legacy task list tools (wrappers around _impl functions) ---
@server.tool() # type: ignore
@require_google_service("tasks", "tasks_read") # type: ignore
@handle_http_errors("list_tasks", service_type="tasks") # type: ignore
@@ -1017,12 +1011,3 @@ async def manage_task(
# --- Legacy task tools (wrappers around _impl functions) ---