apply ruff formatting

This commit is contained in:
Taylor Wilsdon
2025-12-13 13:49:28 -08:00
parent 1d80a24ca4
commit 6b8352a354
50 changed files with 4010 additions and 2842 deletions

View File

@@ -25,26 +25,30 @@ from core.server import server
logger = logging.getLogger(__name__)
def _parse_reminders_json(reminders_input: Optional[Union[str, List[Dict[str, Any]]]], function_name: str) -> List[Dict[str, Any]]:
def _parse_reminders_json(
reminders_input: Optional[Union[str, List[Dict[str, Any]]]], function_name: str
) -> List[Dict[str, Any]]:
"""
Parse reminders from JSON string or list object and validate them.
Args:
reminders_input: JSON string containing reminder objects or list of reminder objects
function_name: Name of calling function for logging
Returns:
List of validated reminder objects
"""
if not reminders_input:
return []
# Handle both string (JSON) and list inputs
if isinstance(reminders_input, str):
try:
reminders = json.loads(reminders_input)
if not isinstance(reminders, list):
logger.warning(f"[{function_name}] Reminders must be a JSON array, got {type(reminders).__name__}")
logger.warning(
f"[{function_name}] Reminders must be a JSON array, got {type(reminders).__name__}"
)
return []
except json.JSONDecodeError as e:
logger.warning(f"[{function_name}] Invalid JSON for reminders: {e}")
@@ -52,35 +56,46 @@ def _parse_reminders_json(reminders_input: Optional[Union[str, List[Dict[str, An
elif isinstance(reminders_input, list):
reminders = reminders_input
else:
logger.warning(f"[{function_name}] Reminders must be a JSON string or list, got {type(reminders_input).__name__}")
logger.warning(
f"[{function_name}] Reminders must be a JSON string or list, got {type(reminders_input).__name__}"
)
return []
# Validate reminders
if len(reminders) > 5:
logger.warning(f"[{function_name}] More than 5 reminders provided, truncating to first 5")
logger.warning(
f"[{function_name}] More than 5 reminders provided, truncating to first 5"
)
reminders = reminders[:5]
validated_reminders = []
for reminder in reminders:
if not isinstance(reminder, dict) or "method" not in reminder or "minutes" not in reminder:
logger.warning(f"[{function_name}] Invalid reminder format: {reminder}, skipping")
if (
not isinstance(reminder, dict)
or "method" not in reminder
or "minutes" not in reminder
):
logger.warning(
f"[{function_name}] Invalid reminder format: {reminder}, skipping"
)
continue
method = reminder["method"].lower()
if method not in ["popup", "email"]:
logger.warning(f"[{function_name}] Invalid reminder method '{method}', must be 'popup' or 'email', skipping")
logger.warning(
f"[{function_name}] Invalid reminder method '{method}', must be 'popup' or 'email', skipping"
)
continue
minutes = reminder["minutes"]
if not isinstance(minutes, int) or minutes < 0 or minutes > 40320:
logger.warning(f"[{function_name}] Invalid reminder minutes '{minutes}', must be integer 0-40320, skipping")
logger.warning(
f"[{function_name}] Invalid reminder minutes '{minutes}', must be integer 0-40320, skipping"
)
continue
validated_reminders.append({
"method": method,
"minutes": minutes
})
validated_reminders.append({"method": method, "minutes": minutes})
return validated_reminders
@@ -136,7 +151,11 @@ def _apply_visibility_if_valid(
)
def _preserve_existing_fields(event_body: Dict[str, Any], existing_event: Dict[str, Any], field_mappings: Dict[str, Any]) -> None:
def _preserve_existing_fields(
event_body: Dict[str, Any],
existing_event: Dict[str, Any],
field_mappings: Dict[str, Any],
) -> None:
"""
Helper function to preserve existing event fields when not explicitly provided.
@@ -153,21 +172,23 @@ def _preserve_existing_fields(event_body: Dict[str, Any], existing_event: Dict[s
event_body[field_name] = new_value
def _format_attendee_details(attendees: List[Dict[str, Any]], indent: str = " ") -> str:
def _format_attendee_details(
attendees: List[Dict[str, Any]], indent: str = " "
) -> str:
"""
Format attendee details including response status, organizer, and optional flags.
Format attendee details including response status, organizer, and optional flags.
Example output format:
" user@example.com: accepted
manager@example.com: declined (organizer)
optional-person@example.com: tentative (optional)"
Example output format:
" user@example.com: accepted
manager@example.com: declined (organizer)
optional-person@example.com: tentative (optional)"
Args:
attendees: List of attendee dictionaries from Google Calendar API
indent: Indentation to use for newline-separated attendees (default: " ")
Args:
attendees: List of attendee dictionaries from Google Calendar API
indent: Indentation to use for newline-separated attendees (default: " ")
Returns:
Formatted string with attendee details, or "None" if no attendees
Returns:
Formatted string with attendee details, or "None" if no attendees
"""
if not attendees:
return "None"
@@ -190,7 +211,9 @@ def _format_attendee_details(attendees: List[Dict[str, Any]], indent: str = " "
return f"\n{indent}".join(attendee_details_list)
def _format_attachment_details(attachments: List[Dict[str, Any]], indent: str = " ") -> str:
def _format_attachment_details(
attachments: List[Dict[str, Any]], indent: str = " "
) -> str:
"""
Format attachment details including file information.
@@ -301,7 +324,7 @@ async def list_calendars(service, user_google_email: str) -> str:
return f"No calendars found for {user_google_email}."
calendars_summary_list = [
f"- \"{cal.get('summary', 'No Summary')}\"{' (Primary)' if cal.get('primary') else ''} (ID: {cal['id']})"
f'- "{cal.get("summary", "No Summary")}"{" (Primary)" if cal.get("primary") else ""} (ID: {cal["id"]})'
for cal in items
]
text_output = (
@@ -353,7 +376,9 @@ async def get_events(
if event_id:
logger.info(f"[get_events] Retrieving single event with ID: {event_id}")
event = await asyncio.to_thread(
lambda: service.events().get(calendarId=calendar_id, eventId=event_id).execute()
lambda: service.events()
.get(calendarId=calendar_id, eventId=event_id)
.execute()
)
items = [event]
else:
@@ -398,9 +423,7 @@ async def get_events(
request_params["q"] = query
events_result = await asyncio.to_thread(
lambda: service.events()
.list(**request_params)
.execute()
lambda: service.events().list(**request_params).execute()
)
items = events_result.get("items", [])
if not items:
@@ -419,30 +442,33 @@ async def get_events(
description = item.get("description", "No Description")
location = item.get("location", "No Location")
attendees = item.get("attendees", [])
attendee_emails = ", ".join([a.get("email", "") for a in attendees]) if attendees else "None"
attendee_emails = (
", ".join([a.get("email", "") for a in attendees]) if attendees else "None"
)
attendee_details_str = _format_attendee_details(attendees, indent=" ")
event_details = (
f'Event Details:\n'
f'- Title: {summary}\n'
f'- Starts: {start}\n'
f'- Ends: {end}\n'
f'- Description: {description}\n'
f'- Location: {location}\n'
f'- Attendees: {attendee_emails}\n'
f'- Attendee Details: {attendee_details_str}\n'
f"Event Details:\n"
f"- Title: {summary}\n"
f"- Starts: {start}\n"
f"- Ends: {end}\n"
f"- Description: {description}\n"
f"- Location: {location}\n"
f"- Attendees: {attendee_emails}\n"
f"- Attendee Details: {attendee_details_str}\n"
)
if include_attachments:
attachments = item.get("attachments", [])
attachment_details_str = _format_attachment_details(attachments, indent=" ")
event_details += f'- Attachments: {attachment_details_str}\n'
attachment_details_str = _format_attachment_details(
attachments, indent=" "
)
event_details += f"- Attachments: {attachment_details_str}\n"
event_details += (
f'- Event ID: {event_id}\n'
f'- Link: {link}'
event_details += f"- Event ID: {event_id}\n- Link: {link}"
logger.info(
f"[get_events] Successfully retrieved detailed event {event_id} for {user_google_email}."
)
logger.info(f"[get_events] Successfully retrieved detailed event {event_id} for {user_google_email}.")
return event_details
# Handle multiple events or single event with basic output
@@ -453,29 +479,35 @@ async def get_events(
end_time = item["end"].get("dateTime", item["end"].get("date"))
link = item.get("htmlLink", "No Link")
item_event_id = item.get("id", "No ID")
if detailed:
# Add detailed information for multiple events
description = item.get("description", "No Description")
location = item.get("location", "No Location")
attendees = item.get("attendees", [])
attendee_emails = ", ".join([a.get("email", "") for a in attendees]) if attendees else "None"
attendee_emails = (
", ".join([a.get("email", "") for a in attendees])
if attendees
else "None"
)
attendee_details_str = _format_attendee_details(attendees, indent=" ")
event_detail_parts = (
f'- "{summary}" (Starts: {start_time}, Ends: {end_time})\n'
f' Description: {description}\n'
f' Location: {location}\n'
f' Attendees: {attendee_emails}\n'
f' Attendee Details: {attendee_details_str}\n'
f" Description: {description}\n"
f" Location: {location}\n"
f" Attendees: {attendee_emails}\n"
f" Attendee Details: {attendee_details_str}\n"
)
if include_attachments:
attachments = item.get("attachments", [])
attachment_details_str = _format_attachment_details(attachments, indent=" ")
event_detail_parts += f' Attachments: {attachment_details_str}\n'
attachment_details_str = _format_attachment_details(
attachments, indent=" "
)
event_detail_parts += f" Attachments: {attachment_details_str}\n"
event_detail_parts += f' ID: {item_event_id} | Link: {link}'
event_detail_parts += f" ID: {item_event_id} | Link: {link}"
event_details_list.append(event_detail_parts)
else:
# Basic output format
@@ -485,14 +517,17 @@ async def get_events(
if event_id:
# Single event basic output
text_output = f"Successfully retrieved event from calendar '{calendar_id}' for {user_google_email}:\n" + "\n".join(event_details_list)
text_output = (
f"Successfully retrieved event from calendar '{calendar_id}' for {user_google_email}:\n"
+ "\n".join(event_details_list)
)
else:
# Multiple events output
text_output = (
f"Successfully retrieved {len(items)} events from calendar '{calendar_id}' for {user_google_email}:\n"
+ "\n".join(event_details_list)
)
logger.info(f"Successfully retrieved {len(items)} events for {user_google_email}.")
return text_output
@@ -547,18 +582,16 @@ async def create_event(
logger.info(f"[create_event] Incoming attachments param: {attachments}")
# If attachments value is a string, split by comma and strip whitespace
if attachments and isinstance(attachments, str):
attachments = [a.strip() for a in attachments.split(',') if a.strip()]
logger.info(f"[create_event] Parsed attachments list from string: {attachments}")
attachments = [a.strip() for a in attachments.split(",") if a.strip()]
logger.info(
f"[create_event] Parsed attachments list from string: {attachments}"
)
event_body: Dict[str, Any] = {
"summary": summary,
"start": (
{"date": start_time}
if "T" not in start_time
else {"dateTime": start_time}
),
"end": (
{"date": end_time} if "T" not in end_time else {"dateTime": end_time}
{"date": start_time} if "T" not in start_time else {"dateTime": start_time}
),
"end": ({"date": end_time} if "T" not in end_time else {"dateTime": end_time}),
}
if location:
event_body["location"] = location
@@ -576,18 +609,20 @@ async def create_event(
if reminders is not None or not use_default_reminders:
# If custom reminders are provided, automatically disable default reminders
effective_use_default = use_default_reminders and reminders is None
reminder_data = {
"useDefault": effective_use_default
}
reminder_data = {"useDefault": effective_use_default}
if reminders is not None:
validated_reminders = _parse_reminders_json(reminders, "create_event")
if validated_reminders:
reminder_data["overrides"] = validated_reminders
logger.info(f"[create_event] Added {len(validated_reminders)} custom reminders")
logger.info(
f"[create_event] Added {len(validated_reminders)} custom reminders"
)
if use_default_reminders:
logger.info("[create_event] Custom reminders provided - disabling default reminders")
logger.info(
"[create_event] Custom reminders provided - disabling default reminders"
)
event_body["reminders"] = reminder_data
# Handle transparency validation
@@ -601,12 +636,12 @@ async def create_event(
event_body["conferenceData"] = {
"createRequest": {
"requestId": request_id,
"conferenceSolutionKey": {
"type": "hangoutsMeet"
}
"conferenceSolutionKey": {"type": "hangoutsMeet"},
}
}
logger.info(f"[create_event] Adding Google Meet conference with request ID: {request_id}")
logger.info(
f"[create_event] Adding Google Meet conference with request ID: {request_id}"
)
if attachments:
# Accept both file URLs and file IDs. If a URL, extract the fileId.
@@ -622,10 +657,14 @@ async def create_event(
# Match /d/<id>, /file/d/<id>, ?id=<id>
match = re.search(r"(?:/d/|/file/d/|id=)([\w-]+)", att)
file_id = match.group(1) if match else None
logger.info(f"[create_event] Extracted file_id '{file_id}' from attachment URL '{att}'")
logger.info(
f"[create_event] Extracted file_id '{file_id}' from attachment URL '{att}'"
)
else:
file_id = att
logger.info(f"[create_event] Using direct file_id '{file_id}' for attachment")
logger.info(
f"[create_event] Using direct file_id '{file_id}' for attachment"
)
if file_id:
file_url = f"https://drive.google.com/open?id={file_id}"
mime_type = "application/vnd.google-apps.drive-sdk"
@@ -634,34 +673,55 @@ async def create_event(
if drive_service:
try:
file_metadata = await asyncio.to_thread(
lambda: drive_service.files().get(fileId=file_id, fields="mimeType,name", supportsAllDrives=True).execute()
lambda: drive_service.files()
.get(
fileId=file_id,
fields="mimeType,name",
supportsAllDrives=True,
)
.execute()
)
mime_type = file_metadata.get("mimeType", mime_type)
filename = file_metadata.get("name")
if filename:
title = filename
logger.info(f"[create_event] Using filename '{filename}' as attachment title")
logger.info(
f"[create_event] Using filename '{filename}' as attachment title"
)
else:
logger.info("[create_event] No filename found, using generic title")
logger.info(
"[create_event] No filename found, using generic title"
)
except Exception as e:
logger.warning(f"Could not fetch metadata for file {file_id}: {e}")
event_body["attachments"].append({
"fileUrl": file_url,
"title": title,
"mimeType": mime_type,
})
logger.warning(
f"Could not fetch metadata for file {file_id}: {e}"
)
event_body["attachments"].append(
{
"fileUrl": file_url,
"title": title,
"mimeType": mime_type,
}
)
created_event = await asyncio.to_thread(
lambda: service.events().insert(
calendarId=calendar_id, body=event_body, supportsAttachments=True,
conferenceDataVersion=1 if add_google_meet else 0
).execute()
lambda: service.events()
.insert(
calendarId=calendar_id,
body=event_body,
supportsAttachments=True,
conferenceDataVersion=1 if add_google_meet else 0,
)
.execute()
)
else:
created_event = await asyncio.to_thread(
lambda: service.events().insert(
calendarId=calendar_id, body=event_body,
conferenceDataVersion=1 if add_google_meet else 0
).execute()
lambda: service.events()
.insert(
calendarId=calendar_id,
body=event_body,
conferenceDataVersion=1 if add_google_meet else 0,
)
.execute()
)
link = created_event.get("htmlLink", "No link available")
confirmation_message = f"Successfully created event '{created_event.get('summary', summary)}' for {user_google_email}. Link: {link}"
@@ -678,8 +738,8 @@ async def create_event(
break
logger.info(
f"Event created successfully for {user_google_email}. ID: {created_event.get('id')}, Link: {link}"
)
f"Event created successfully for {user_google_email}. ID: {created_event.get('id')}, Link: {link}"
)
return confirmation_message
@@ -737,9 +797,7 @@ async def modify_event(
event_body["summary"] = summary
if start_time is not None:
event_body["start"] = (
{"date": start_time}
if "T" not in start_time
else {"dateTime": start_time}
{"date": start_time} if "T" not in start_time else {"dateTime": start_time}
)
if timezone is not None and "dateTime" in event_body["start"]:
event_body["start"]["timeZone"] = timezone
@@ -755,7 +813,7 @@ async def modify_event(
event_body["location"] = location
if attendees is not None:
event_body["attendees"] = [{"email": email} for email in attendees]
# Handle reminders
if reminders is not None or use_default_reminders is not None:
reminder_data = {}
@@ -764,25 +822,41 @@ async def modify_event(
else:
# Preserve existing event's useDefault value if not explicitly specified
try:
existing_event = service.events().get(calendarId=calendar_id, eventId=event_id).execute()
reminder_data["useDefault"] = existing_event.get("reminders", {}).get("useDefault", True)
existing_event = (
service.events()
.get(calendarId=calendar_id, eventId=event_id)
.execute()
)
reminder_data["useDefault"] = existing_event.get("reminders", {}).get(
"useDefault", True
)
except Exception as e:
logger.warning(f"[modify_event] Could not fetch existing event for reminders: {e}")
reminder_data["useDefault"] = True # Fallback to True if unable to fetch
logger.warning(
f"[modify_event] Could not fetch existing event for reminders: {e}"
)
reminder_data["useDefault"] = (
True # Fallback to True if unable to fetch
)
# If custom reminders are provided, automatically disable default reminders
if reminders is not None:
if reminder_data.get("useDefault", False):
reminder_data["useDefault"] = False
logger.info("[modify_event] Custom reminders provided - disabling default reminders")
logger.info(
"[modify_event] Custom reminders provided - disabling default reminders"
)
validated_reminders = _parse_reminders_json(reminders, "modify_event")
if reminders and not validated_reminders:
logger.warning("[modify_event] Reminders provided but failed validation. No custom reminders will be set.")
logger.warning(
"[modify_event] Reminders provided but failed validation. No custom reminders will be set."
)
elif validated_reminders:
reminder_data["overrides"] = validated_reminders
logger.info(f"[modify_event] Updated reminders with {len(validated_reminders)} custom reminders")
logger.info(
f"[modify_event] Updated reminders with {len(validated_reminders)} custom reminders"
)
event_body["reminders"] = reminder_data
# Handle transparency validation
@@ -791,11 +865,7 @@ async def modify_event(
# Handle visibility validation
_apply_visibility_if_valid(event_body, visibility, "modify_event")
if (
timezone is not None
and "start" not in event_body
and "end" not in event_body
):
if timezone is not None and "start" not in event_body and "end" not in event_body:
# If timezone is provided but start/end times are not, we need to fetch the existing event
# to apply the timezone correctly. This is a simplification; a full implementation
# might handle this more robustly or require start/end with timezone.
@@ -817,19 +887,25 @@ async def modify_event(
# Get the existing event to preserve fields that aren't being updated
try:
existing_event = await asyncio.to_thread(
lambda: service.events().get(calendarId=calendar_id, eventId=event_id).execute()
lambda: service.events()
.get(calendarId=calendar_id, eventId=event_id)
.execute()
)
logger.info(
"[modify_event] Successfully retrieved existing event before update"
)
# Preserve existing fields if not provided in the update
_preserve_existing_fields(event_body, existing_event, {
"summary": summary,
"description": description,
"location": location,
"attendees": attendees
})
_preserve_existing_fields(
event_body,
existing_event,
{
"summary": summary,
"description": description,
"location": location,
"attendees": attendees,
},
)
# Handle Google Meet conference data
if add_google_meet is not None:
@@ -839,17 +915,17 @@ async def modify_event(
event_body["conferenceData"] = {
"createRequest": {
"requestId": request_id,
"conferenceSolutionKey": {
"type": "hangoutsMeet"
}
"conferenceSolutionKey": {"type": "hangoutsMeet"},
}
}
logger.info(f"[modify_event] Adding Google Meet conference with request ID: {request_id}")
logger.info(
f"[modify_event] Adding Google Meet conference with request ID: {request_id}"
)
else:
# Remove Google Meet by setting conferenceData to empty
event_body["conferenceData"] = {}
logger.info("[modify_event] Removing Google Meet conference")
elif 'conferenceData' in existing_event:
elif "conferenceData" in existing_event:
# Preserve existing conference data if not specified
event_body["conferenceData"] = existing_event["conferenceData"]
logger.info("[modify_event] Preserving existing conference data")
@@ -869,7 +945,12 @@ async def modify_event(
# Proceed with the update
updated_event = await asyncio.to_thread(
lambda: service.events()
.update(calendarId=calendar_id, eventId=event_id, body=event_body, conferenceDataVersion=1)
.update(
calendarId=calendar_id,
eventId=event_id,
body=event_body,
conferenceDataVersion=1,
)
.execute()
)
@@ -898,7 +979,9 @@ async def modify_event(
@server.tool()
@handle_http_errors("delete_event", service_type="calendar")
@require_google_service("calendar", "calendar_events")
async def delete_event(service, user_google_email: str, event_id: str, calendar_id: str = "primary") -> str:
async def delete_event(
service, user_google_email: str, event_id: str, calendar_id: str = "primary"
) -> str:
"""
Deletes an existing event.
@@ -922,11 +1005,11 @@ async def delete_event(service, user_google_email: str, event_id: str, calendar_
# Try to get the event first to verify it exists
try:
await asyncio.to_thread(
lambda: service.events().get(calendarId=calendar_id, eventId=event_id).execute()
)
logger.info(
"[delete_event] Successfully verified event exists before deletion"
lambda: service.events()
.get(calendarId=calendar_id, eventId=event_id)
.execute()
)
logger.info("[delete_event] Successfully verified event exists before deletion")
except HttpError as get_error:
if get_error.resp.status == 404:
logger.error(
@@ -941,7 +1024,9 @@ async def delete_event(service, user_google_email: str, event_id: str, calendar_
# Proceed with the deletion
await asyncio.to_thread(
lambda: service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
lambda: service.events()
.delete(calendarId=calendar_id, eventId=event_id)
.execute()
)
confirmation_message = f"Successfully deleted event (ID: {event_id}) from calendar '{calendar_id}' for {user_google_email}."