Skip to content

Commit 0f80ce1

Browse files
committed
Revert "fix: address Copilot review feedback on PR #416"
This reverts commit e5a746d.
1 parent e5a746d commit 0f80ce1

3 files changed

Lines changed: 216 additions & 67 deletions

File tree

gcalendar/calendar_tools.py

Lines changed: 209 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,9 @@ 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,
557560
) -> str:
558561
"""
559562
Creates a new event.
@@ -574,6 +577,9 @@ async def create_event(
574577
use_default_reminders (bool): Whether to use calendar's default reminders. If False, uses custom reminders. Defaults to True.
575578
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).
576579
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).
577583
578584
Returns:
579585
str: Confirmation message of the successful event creation with event link.
@@ -633,6 +639,21 @@ async def create_event(
633639
# Handle visibility validation
634640
_apply_visibility_if_valid(event_body, visibility, "create_event")
635641

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+
636657
if add_google_meet:
637658
request_id = str(uuid.uuid4())
638659
event_body["conferenceData"] = {
@@ -650,61 +671,69 @@ async def create_event(
650671
event_body["attachments"] = []
651672
drive_service = None
652673
try:
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}'"
674+
try:
675+
drive_service = service._http and build(
676+
"drive", "v3", http=service._http
664677
)
665-
else:
666-
file_id = att
667-
logger.info(
668-
f"[create_event] Using direct file_id '{file_id}' for attachment"
678+
except Exception as e:
679+
logger.warning(
680+
f"Could not build Drive service for MIME type lookup: {e}"
669681
)
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,
683-
)
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"
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()
692711
)
693-
else:
694-
logger.info(
695-
"[create_event] No filename found, using generic title"
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}"
696726
)
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-
)
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()
708737
created_event = await asyncio.to_thread(
709738
lambda: service.events()
710739
.insert(
@@ -795,6 +824,9 @@ async def modify_event(
795824
transparency: Optional[str] = None,
796825
visibility: Optional[str] = None,
797826
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,
798830
) -> str:
799831
"""
800832
Modifies an existing event.
@@ -816,6 +848,9 @@ async def modify_event(
816848
transparency (Optional[str]): Event transparency for busy/free status. "opaque" shows as Busy, "transparent" shows as Available/Free. If None, preserves existing transparency setting.
817849
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.
818850
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.
819854
820855
Returns:
821856
str: Confirmation message of the successful event modification with event link.
@@ -904,6 +939,21 @@ async def modify_event(
904939
# Handle visibility validation
905940
_apply_visibility_if_valid(event_body, visibility, "modify_event")
906941

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+
907957
if timezone is not None and "start" not in event_body and "end" not in event_body:
908958
# If timezone is provided but start/end times are not, we need to fetch the existing event
909959
# to apply the timezone correctly. This is a simplification; a full implementation
@@ -1073,3 +1123,111 @@ async def delete_event(
10731123
confirmation_message = f"Successfully deleted event (ID: {event_id}) from calendar '{calendar_id}' for {user_google_email}."
10741124
logger.info(f"Event deleted successfully for {user_google_email}. ID: {event_id}")
10751125
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 color {background_color}")
577+
applied_parts.append(f"background {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: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -338,11 +338,8 @@ async def test_format_invalid_wrap_strategy():
338338
wrap_strategy="INVALID",
339339
)
340340

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
341+
error_msg = str(exc_info.value).lower()
342+
assert "wrap_strategy" in error_msg or "wrap" in error_msg
346343

347344

348345
@pytest.mark.asyncio
@@ -360,11 +357,8 @@ async def test_format_invalid_horizontal_alignment():
360357
horizontal_alignment="INVALID",
361358
)
362359

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
360+
error_msg = str(exc_info.value).lower()
361+
assert "horizontal" in error_msg or "left" in error_msg
368362

369363

370364
@pytest.mark.asyncio
@@ -382,11 +376,8 @@ async def test_format_invalid_vertical_alignment():
382376
vertical_alignment="INVALID",
383377
)
384378

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
379+
error_msg = str(exc_info.value).lower()
380+
assert "vertical" in error_msg or "top" in error_msg
390381

391382

392383
@pytest.mark.asyncio

0 commit comments

Comments
 (0)