Skip to content

Commit e5a746d

Browse files
Arnoclaude
authored andcommitted
fix: address Copilot review feedback on PR #416
- Use consistent terminology in confirmation message ("background color" to match "text color") - Tighten test assertions for invalid parameter validation to check parameter name and all allowed values instead of loose substring matches - Remove unrelated calendar_tools.py changes (lambda reformatting, guest permissions, query_freebusy) that belong in separate PRs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 817db85 commit e5a746d

3 files changed

Lines changed: 67 additions & 216 deletions

File tree

gcalendar/calendar_tools.py

Lines changed: 51 additions & 209 deletions
Original file line numberDiff line numberDiff line change
@@ -554,9 +554,6 @@ async def create_event(
554554
use_default_reminders: bool = True,
555555
transparency: Optional[str] = None,
556556
visibility: Optional[str] = None,
557-
guests_can_modify: Optional[bool] = None,
558-
guests_can_invite_others: Optional[bool] = None,
559-
guests_can_see_other_guests: Optional[bool] = None,
560557
) -> str:
561558
"""
562559
Creates a new event.
@@ -577,9 +574,6 @@ async def create_event(
577574
use_default_reminders (bool): Whether to use calendar's default reminders. If False, uses custom reminders. Defaults to True.
578575
transparency (Optional[str]): Event transparency for busy/free status. "opaque" shows as Busy (default), "transparent" shows as Available/Free. Defaults to None (uses Google Calendar default).
579576
visibility (Optional[str]): Event visibility. "default" uses calendar default, "public" is visible to all, "private" is visible only to attendees, "confidential" is same as private (legacy). Defaults to None (uses Google Calendar default).
580-
guests_can_modify (Optional[bool]): Whether attendees other than the organizer can modify the event. Defaults to None (uses Google Calendar default of False).
581-
guests_can_invite_others (Optional[bool]): Whether attendees other than the organizer can invite others to the event. Defaults to None (uses Google Calendar default of True).
582-
guests_can_see_other_guests (Optional[bool]): Whether attendees other than the organizer can see who the event's attendees are. Defaults to None (uses Google Calendar default of True).
583577
584578
Returns:
585579
str: Confirmation message of the successful event creation with event link.
@@ -639,21 +633,6 @@ async def create_event(
639633
# Handle visibility validation
640634
_apply_visibility_if_valid(event_body, visibility, "create_event")
641635

642-
# Handle guest permissions
643-
if guests_can_modify is not None:
644-
event_body["guestsCanModify"] = guests_can_modify
645-
logger.info(f"[create_event] Set guestsCanModify to {guests_can_modify}")
646-
if guests_can_invite_others is not None:
647-
event_body["guestsCanInviteOthers"] = guests_can_invite_others
648-
logger.info(
649-
f"[create_event] Set guestsCanInviteOthers to {guests_can_invite_others}"
650-
)
651-
if guests_can_see_other_guests is not None:
652-
event_body["guestsCanSeeOtherGuests"] = guests_can_see_other_guests
653-
logger.info(
654-
f"[create_event] Set guestsCanSeeOtherGuests to {guests_can_see_other_guests}"
655-
)
656-
657636
if add_google_meet:
658637
request_id = str(uuid.uuid4())
659638
event_body["conferenceData"] = {
@@ -671,69 +650,61 @@ async def create_event(
671650
event_body["attachments"] = []
672651
drive_service = None
673652
try:
674-
try:
675-
drive_service = service._http and build(
676-
"drive", "v3", http=service._http
653+
drive_service = service._http and build("drive", "v3", http=service._http)
654+
except Exception as e:
655+
logger.warning(f"Could not build Drive service for MIME type lookup: {e}")
656+
for att in attachments:
657+
file_id = None
658+
if att.startswith("https://"):
659+
# Match /d/<id>, /file/d/<id>, ?id=<id>
660+
match = re.search(r"(?:/d/|/file/d/|id=)([\w-]+)", att)
661+
file_id = match.group(1) if match else None
662+
logger.info(
663+
f"[create_event] Extracted file_id '{file_id}' from attachment URL '{att}'"
677664
)
678-
except Exception as e:
679-
logger.warning(
680-
f"Could not build Drive service for MIME type lookup: {e}"
665+
else:
666+
file_id = att
667+
logger.info(
668+
f"[create_event] Using direct file_id '{file_id}' for attachment"
681669
)
682-
for att in attachments:
683-
file_id = None
684-
if att.startswith("https://"):
685-
# Match /d/<id>, /file/d/<id>, ?id=<id>
686-
match = re.search(r"(?:/d/|/file/d/|id=)([\w-]+)", att)
687-
file_id = match.group(1) if match else None
688-
logger.info(
689-
f"[create_event] Extracted file_id '{file_id}' from attachment URL '{att}'"
690-
)
691-
else:
692-
file_id = att
693-
logger.info(
694-
f"[create_event] Using direct file_id '{file_id}' for attachment"
695-
)
696-
if file_id:
697-
file_url = f"https://drive.google.com/open?id={file_id}"
698-
mime_type = "application/vnd.google-apps.drive-sdk"
699-
title = "Drive Attachment"
700-
# Try to get the actual MIME type and filename from Drive
701-
if drive_service:
702-
try:
703-
file_metadata = await asyncio.to_thread(
704-
lambda: drive_service.files()
705-
.get(
706-
fileId=file_id,
707-
fields="mimeType,name",
708-
supportsAllDrives=True,
709-
)
710-
.execute()
670+
if file_id:
671+
file_url = f"https://drive.google.com/open?id={file_id}"
672+
mime_type = "application/vnd.google-apps.drive-sdk"
673+
title = "Drive Attachment"
674+
# Try to get the actual MIME type and filename from Drive
675+
if drive_service:
676+
try:
677+
file_metadata = await asyncio.to_thread(
678+
lambda: drive_service.files()
679+
.get(
680+
fileId=file_id,
681+
fields="mimeType,name",
682+
supportsAllDrives=True,
711683
)
712-
mime_type = file_metadata.get("mimeType", mime_type)
713-
filename = file_metadata.get("name")
714-
if filename:
715-
title = filename
716-
logger.info(
717-
f"[create_event] Using filename '{filename}' as attachment title"
718-
)
719-
else:
720-
logger.info(
721-
"[create_event] No filename found, using generic title"
722-
)
723-
except Exception as e:
724-
logger.warning(
725-
f"Could not fetch metadata for file {file_id}: {e}"
684+
.execute()
685+
)
686+
mime_type = file_metadata.get("mimeType", mime_type)
687+
filename = file_metadata.get("name")
688+
if filename:
689+
title = filename
690+
logger.info(
691+
f"[create_event] Using filename '{filename}' as attachment title"
726692
)
727-
event_body["attachments"].append(
728-
{
729-
"fileUrl": file_url,
730-
"title": title,
731-
"mimeType": mime_type,
732-
}
733-
)
734-
finally:
735-
if drive_service:
736-
drive_service.close()
693+
else:
694+
logger.info(
695+
"[create_event] No filename found, using generic title"
696+
)
697+
except Exception as e:
698+
logger.warning(
699+
f"Could not fetch metadata for file {file_id}: {e}"
700+
)
701+
event_body["attachments"].append(
702+
{
703+
"fileUrl": file_url,
704+
"title": title,
705+
"mimeType": mime_type,
706+
}
707+
)
737708
created_event = await asyncio.to_thread(
738709
lambda: service.events()
739710
.insert(
@@ -824,9 +795,6 @@ async def modify_event(
824795
transparency: Optional[str] = None,
825796
visibility: Optional[str] = None,
826797
color_id: Optional[str] = None,
827-
guests_can_modify: Optional[bool] = None,
828-
guests_can_invite_others: Optional[bool] = None,
829-
guests_can_see_other_guests: Optional[bool] = None,
830798
) -> str:
831799
"""
832800
Modifies an existing event.
@@ -848,9 +816,6 @@ async def modify_event(
848816
transparency (Optional[str]): Event transparency for busy/free status. "opaque" shows as Busy, "transparent" shows as Available/Free. If None, preserves existing transparency setting.
849817
visibility (Optional[str]): Event visibility. "default" uses calendar default, "public" is visible to all, "private" is visible only to attendees, "confidential" is same as private (legacy). If None, preserves existing visibility setting.
850818
color_id (Optional[str]): Event color ID (1-11). If None, preserves existing color.
851-
guests_can_modify (Optional[bool]): Whether attendees other than the organizer can modify the event. If None, preserves existing setting.
852-
guests_can_invite_others (Optional[bool]): Whether attendees other than the organizer can invite others to the event. If None, preserves existing setting.
853-
guests_can_see_other_guests (Optional[bool]): Whether attendees other than the organizer can see who the event's attendees are. If None, preserves existing setting.
854819
855820
Returns:
856821
str: Confirmation message of the successful event modification with event link.
@@ -939,21 +904,6 @@ async def modify_event(
939904
# Handle visibility validation
940905
_apply_visibility_if_valid(event_body, visibility, "modify_event")
941906

942-
# Handle guest permissions
943-
if guests_can_modify is not None:
944-
event_body["guestsCanModify"] = guests_can_modify
945-
logger.info(f"[modify_event] Set guestsCanModify to {guests_can_modify}")
946-
if guests_can_invite_others is not None:
947-
event_body["guestsCanInviteOthers"] = guests_can_invite_others
948-
logger.info(
949-
f"[modify_event] Set guestsCanInviteOthers to {guests_can_invite_others}"
950-
)
951-
if guests_can_see_other_guests is not None:
952-
event_body["guestsCanSeeOtherGuests"] = guests_can_see_other_guests
953-
logger.info(
954-
f"[modify_event] Set guestsCanSeeOtherGuests to {guests_can_see_other_guests}"
955-
)
956-
957907
if timezone is not None and "start" not in event_body and "end" not in event_body:
958908
# If timezone is provided but start/end times are not, we need to fetch the existing event
959909
# to apply the timezone correctly. This is a simplification; a full implementation
@@ -1123,111 +1073,3 @@ async def delete_event(
11231073
confirmation_message = f"Successfully deleted event (ID: {event_id}) from calendar '{calendar_id}' for {user_google_email}."
11241074
logger.info(f"Event deleted successfully for {user_google_email}. ID: {event_id}")
11251075
return confirmation_message
1126-
1127-
1128-
@server.tool()
1129-
@handle_http_errors("query_freebusy", is_read_only=True, service_type="calendar")
1130-
@require_google_service("calendar", "calendar_read")
1131-
async def query_freebusy(
1132-
service,
1133-
user_google_email: str,
1134-
time_min: str,
1135-
time_max: str,
1136-
calendar_ids: Optional[List[str]] = None,
1137-
group_expansion_max: Optional[int] = None,
1138-
calendar_expansion_max: Optional[int] = None,
1139-
) -> str:
1140-
"""
1141-
Returns free/busy information for a set of calendars.
1142-
1143-
Args:
1144-
user_google_email (str): The user's Google email address. Required.
1145-
time_min (str): The start of the interval for the query in RFC3339 format (e.g., '2024-05-12T10:00:00Z' or '2024-05-12').
1146-
time_max (str): The end of the interval for the query in RFC3339 format (e.g., '2024-05-12T18:00:00Z' or '2024-05-12').
1147-
calendar_ids (Optional[List[str]]): List of calendar identifiers to query. If not provided, queries the primary calendar. Use 'primary' for the user's primary calendar or specific calendar IDs obtained from `list_calendars`.
1148-
group_expansion_max (Optional[int]): Maximum number of calendar identifiers to be provided for a single group. Optional. An error is returned for a group with more members than this value. Maximum value is 100.
1149-
calendar_expansion_max (Optional[int]): Maximum number of calendars for which FreeBusy information is to be provided. Optional. Maximum value is 50.
1150-
1151-
Returns:
1152-
str: A formatted response showing free/busy information for each requested calendar, including busy time periods.
1153-
"""
1154-
logger.info(
1155-
f"[query_freebusy] Invoked. Email: '{user_google_email}', time_min: '{time_min}', time_max: '{time_max}'"
1156-
)
1157-
1158-
# Format time parameters
1159-
formatted_time_min = _correct_time_format_for_api(time_min, "time_min")
1160-
formatted_time_max = _correct_time_format_for_api(time_max, "time_max")
1161-
1162-
# Default to primary calendar if no calendar IDs provided
1163-
if not calendar_ids:
1164-
calendar_ids = ["primary"]
1165-
1166-
# Build the request body
1167-
request_body: Dict[str, Any] = {
1168-
"timeMin": formatted_time_min,
1169-
"timeMax": formatted_time_max,
1170-
"items": [{"id": cal_id} for cal_id in calendar_ids],
1171-
}
1172-
1173-
if group_expansion_max is not None:
1174-
request_body["groupExpansionMax"] = group_expansion_max
1175-
if calendar_expansion_max is not None:
1176-
request_body["calendarExpansionMax"] = calendar_expansion_max
1177-
1178-
logger.info(
1179-
f"[query_freebusy] Request body: timeMin={formatted_time_min}, timeMax={formatted_time_max}, calendars={calendar_ids}"
1180-
)
1181-
1182-
# Execute the freebusy query
1183-
freebusy_result = await asyncio.to_thread(
1184-
lambda: service.freebusy().query(body=request_body).execute()
1185-
)
1186-
1187-
# Parse the response
1188-
calendars = freebusy_result.get("calendars", {})
1189-
time_min_result = freebusy_result.get("timeMin", formatted_time_min)
1190-
time_max_result = freebusy_result.get("timeMax", formatted_time_max)
1191-
1192-
if not calendars:
1193-
return f"No free/busy information found for the requested calendars for {user_google_email}."
1194-
1195-
# Format the output
1196-
output_lines = [
1197-
f"Free/Busy information for {user_google_email}:",
1198-
f"Time range: {time_min_result} to {time_max_result}",
1199-
"",
1200-
]
1201-
1202-
for cal_id, cal_data in calendars.items():
1203-
output_lines.append(f"Calendar: {cal_id}")
1204-
1205-
# Check for errors
1206-
errors = cal_data.get("errors", [])
1207-
if errors:
1208-
output_lines.append(" Errors:")
1209-
for error in errors:
1210-
domain = error.get("domain", "unknown")
1211-
reason = error.get("reason", "unknown")
1212-
output_lines.append(f" - {domain}: {reason}")
1213-
output_lines.append("")
1214-
continue
1215-
1216-
# Get busy periods
1217-
busy_periods = cal_data.get("busy", [])
1218-
if not busy_periods:
1219-
output_lines.append(" Status: Free (no busy periods)")
1220-
else:
1221-
output_lines.append(f" Busy periods: {len(busy_periods)}")
1222-
for period in busy_periods:
1223-
start = period.get("start", "Unknown")
1224-
end = period.get("end", "Unknown")
1225-
output_lines.append(f" - {start} to {end}")
1226-
1227-
output_lines.append("")
1228-
1229-
result_text = "\n".join(output_lines)
1230-
logger.info(
1231-
f"[query_freebusy] Successfully retrieved free/busy information for {len(calendars)} calendar(s)"
1232-
)
1233-
return result_text

gsheets/sheets_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ async def _format_sheet_range_impl(
574574
# Build confirmation message
575575
applied_parts = []
576576
if bg_color_parsed:
577-
applied_parts.append(f"background {background_color}")
577+
applied_parts.append(f"background color {background_color}")
578578
if text_color_parsed:
579579
applied_parts.append(f"text color {text_color}")
580580
if number_format:

tests/gsheets/test_format_sheet_range.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -338,8 +338,11 @@ async def test_format_invalid_wrap_strategy():
338338
wrap_strategy="INVALID",
339339
)
340340

341-
error_msg = str(exc_info.value).lower()
342-
assert "wrap_strategy" in error_msg or "wrap" in error_msg
341+
error_msg = str(exc_info.value)
342+
assert "wrap_strategy" in error_msg
343+
assert "CLIP" in error_msg
344+
assert "OVERFLOW_CELL" in error_msg
345+
assert "WRAP" in error_msg
343346

344347

345348
@pytest.mark.asyncio
@@ -357,8 +360,11 @@ async def test_format_invalid_horizontal_alignment():
357360
horizontal_alignment="INVALID",
358361
)
359362

360-
error_msg = str(exc_info.value).lower()
361-
assert "horizontal" in error_msg or "left" in error_msg
363+
error_msg = str(exc_info.value)
364+
assert "horizontal_alignment" in error_msg
365+
assert "CENTER" in error_msg
366+
assert "LEFT" in error_msg
367+
assert "RIGHT" in error_msg
362368

363369

364370
@pytest.mark.asyncio
@@ -376,8 +382,11 @@ async def test_format_invalid_vertical_alignment():
376382
vertical_alignment="INVALID",
377383
)
378384

379-
error_msg = str(exc_info.value).lower()
380-
assert "vertical" in error_msg or "top" in error_msg
385+
error_msg = str(exc_info.value)
386+
assert "vertical_alignment" in error_msg
387+
assert "BOTTOM" in error_msg
388+
assert "MIDDLE" in error_msg
389+
assert "TOP" in error_msg
381390

382391

383392
@pytest.mark.asyncio

0 commit comments

Comments
 (0)