From e441ade35f68217a6aa7b6afa9b0d7bacb8b2ef1 Mon Sep 17 00:00:00 2001 From: Rein Lemmens Date: Thu, 12 Mar 2026 21:08:16 +0100 Subject: [PATCH 1/5] Display meeting links in calendar event output Conference/meeting links (Google Meet, Zoom, etc.) were fetched from the API but never included in get_events() output. Extract video entry point from conferenceData (with hangoutLink fallback) and display it in all three output modes: single detailed, multi detailed, and basic list. Co-Authored-By: Claude Opus 4.6 --- gcalendar/calendar_tools.py | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/gcalendar/calendar_tools.py b/gcalendar/calendar_tools.py index 13da74c..60b366d 100644 --- a/gcalendar/calendar_tools.py +++ b/gcalendar/calendar_tools.py @@ -172,6 +172,21 @@ def _preserve_existing_fields( event_body[field_name] = new_value +def _get_meeting_link(item: Dict[str, Any]) -> str: + """Extract video meeting link from event conference data or hangoutLink.""" + conference_data = item.get("conferenceData") + if conference_data and "entryPoints" in conference_data: + for entry_point in conference_data["entryPoints"]: + if entry_point.get("entryPointType") == "video": + uri = entry_point.get("uri", "") + if uri: + return uri + hangout_link = item.get("hangoutLink", "") + if hangout_link: + return hangout_link + return "" + + def _format_attendee_details( attendees: List[Dict[str, Any]], indent: str = " " ) -> str: @@ -448,6 +463,8 @@ async def get_events( ) attendee_details_str = _format_attendee_details(attendees, indent=" ") + meeting_link = _get_meeting_link(item) + event_details = ( f"Event Details:\n" f"- Title: {summary}\n" @@ -456,6 +473,10 @@ async def get_events( f"- Description: {description}\n" f"- Location: {location}\n" f"- Color ID: {color_id}\n" + ) + if meeting_link: + event_details += f"- Meeting Link: {meeting_link}\n" + event_details += ( f"- Attendees: {attendee_emails}\n" f"- Attendee Details: {attendee_details_str}\n" ) @@ -494,10 +515,16 @@ async def get_events( ) attendee_details_str = _format_attendee_details(attendees, indent=" ") + meeting_link = _get_meeting_link(item) + event_detail_parts = ( f'- "{summary}" (Starts: {start_time}, Ends: {end_time})\n' f" Description: {description}\n" f" Location: {location}\n" + ) + if meeting_link: + event_detail_parts += f" Meeting Link: {meeting_link}\n" + event_detail_parts += ( f" Attendees: {attendee_emails}\n" f" Attendee Details: {attendee_details_str}\n" ) @@ -513,9 +540,12 @@ async def get_events( event_details_list.append(event_detail_parts) else: # Basic output format - event_details_list.append( - f'- "{summary}" (Starts: {start_time}, Ends: {end_time}) ID: {item_event_id} | Link: {link}' - ) + meeting_link = _get_meeting_link(item) + basic_line = f'- "{summary}" (Starts: {start_time}, Ends: {end_time})' + if meeting_link: + basic_line += f" Meeting: {meeting_link}" + basic_line += f" ID: {item_event_id} | Link: {link}" + event_details_list.append(basic_line) if event_id: # Single event basic output From 00796f39c6a08e49b78cb6d9a9c1b1fb91aeeeac Mon Sep 17 00:00:00 2001 From: Rein Lemmens Date: Thu, 12 Mar 2026 21:15:39 +0100 Subject: [PATCH 2/5] Extract shared named style constant and use keyword args - Extract VALID_NAMED_STYLE_TYPES constant in docs_helpers.py, reuse in validation_manager.py - Switch build_paragraph_style call to keyword arguments for clarity and resilience Co-Authored-By: Claude Opus 4.6 --- gdocs/docs_helpers.py | 39 ++++++++++++++++------------ gdocs/managers/validation_manager.py | 11 +++----- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/gdocs/docs_helpers.py b/gdocs/docs_helpers.py index 85354f7..05d7705 100644 --- a/gdocs/docs_helpers.py +++ b/gdocs/docs_helpers.py @@ -10,6 +10,18 @@ from typing import Dict, Any, Optional logger = logging.getLogger(__name__) +VALID_NAMED_STYLE_TYPES = ( + "NORMAL_TEXT", + "TITLE", + "SUBTITLE", + "HEADING_1", + "HEADING_2", + "HEADING_3", + "HEADING_4", + "HEADING_5", + "HEADING_6", +) + def _normalize_color( color: Optional[str], param_name: str @@ -137,15 +149,10 @@ def build_paragraph_style( fields = [] if named_style_type is not None: - valid_styles = [ - "NORMAL_TEXT", "TITLE", "SUBTITLE", - "HEADING_1", "HEADING_2", "HEADING_3", - "HEADING_4", "HEADING_5", "HEADING_6", - ] - if named_style_type not in valid_styles: + if named_style_type not in VALID_NAMED_STYLE_TYPES: raise ValueError( f"Invalid named_style_type '{named_style_type}'. " - f"Must be one of: {', '.join(valid_styles)}" + f"Must be one of: {', '.join(VALID_NAMED_STYLE_TYPES)}" ) paragraph_style["namedStyleType"] = named_style_type fields.append("namedStyleType") @@ -360,15 +367,15 @@ def create_update_paragraph_style_request( Dictionary representing the updateParagraphStyle request, or None if no styles provided """ paragraph_style, fields = build_paragraph_style( - heading_level, - alignment, - line_spacing, - indent_first_line, - indent_start, - indent_end, - space_above, - space_below, - named_style_type, + heading_level=heading_level, + alignment=alignment, + line_spacing=line_spacing, + indent_first_line=indent_first_line, + indent_start=indent_start, + indent_end=indent_end, + space_above=space_above, + space_below=space_below, + named_style_type=named_style_type, ) if not paragraph_style: diff --git a/gdocs/managers/validation_manager.py b/gdocs/managers/validation_manager.py index 3235c1a..7b13c21 100644 --- a/gdocs/managers/validation_manager.py +++ b/gdocs/managers/validation_manager.py @@ -9,7 +9,7 @@ import logging from typing import Dict, Any, List, Tuple, Optional from urllib.parse import urlparse -from gdocs.docs_helpers import validate_operation +from gdocs.docs_helpers import validate_operation, VALID_NAMED_STYLE_TYPES logger = logging.getLogger(__name__) @@ -317,15 +317,10 @@ class ValidationManager: ) if named_style_type is not None: - valid_styles = [ - "NORMAL_TEXT", "TITLE", "SUBTITLE", - "HEADING_1", "HEADING_2", "HEADING_3", - "HEADING_4", "HEADING_5", "HEADING_6", - ] - if named_style_type not in valid_styles: + if named_style_type not in VALID_NAMED_STYLE_TYPES: return ( False, - f"Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(valid_styles)}", + f"Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(VALID_NAMED_STYLE_TYPES)}", ) if heading_level is not None: From 9285a5f97ec166688e65451dd941c17c80af4fab Mon Sep 17 00:00:00 2001 From: Rein Lemmens Date: Thu, 12 Mar 2026 21:15:57 +0100 Subject: [PATCH 3/5] Revert "Extract shared named style constant and use keyword args" This reverts commit 00796f39c6a08e49b78cb6d9a9c1b1fb91aeeeac. --- gdocs/docs_helpers.py | 39 ++++++++++++---------------- gdocs/managers/validation_manager.py | 11 +++++--- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/gdocs/docs_helpers.py b/gdocs/docs_helpers.py index 05d7705..85354f7 100644 --- a/gdocs/docs_helpers.py +++ b/gdocs/docs_helpers.py @@ -10,18 +10,6 @@ from typing import Dict, Any, Optional logger = logging.getLogger(__name__) -VALID_NAMED_STYLE_TYPES = ( - "NORMAL_TEXT", - "TITLE", - "SUBTITLE", - "HEADING_1", - "HEADING_2", - "HEADING_3", - "HEADING_4", - "HEADING_5", - "HEADING_6", -) - def _normalize_color( color: Optional[str], param_name: str @@ -149,10 +137,15 @@ def build_paragraph_style( fields = [] if named_style_type is not None: - if named_style_type not in VALID_NAMED_STYLE_TYPES: + valid_styles = [ + "NORMAL_TEXT", "TITLE", "SUBTITLE", + "HEADING_1", "HEADING_2", "HEADING_3", + "HEADING_4", "HEADING_5", "HEADING_6", + ] + if named_style_type not in valid_styles: raise ValueError( f"Invalid named_style_type '{named_style_type}'. " - f"Must be one of: {', '.join(VALID_NAMED_STYLE_TYPES)}" + f"Must be one of: {', '.join(valid_styles)}" ) paragraph_style["namedStyleType"] = named_style_type fields.append("namedStyleType") @@ -367,15 +360,15 @@ def create_update_paragraph_style_request( Dictionary representing the updateParagraphStyle request, or None if no styles provided """ paragraph_style, fields = build_paragraph_style( - heading_level=heading_level, - alignment=alignment, - line_spacing=line_spacing, - indent_first_line=indent_first_line, - indent_start=indent_start, - indent_end=indent_end, - space_above=space_above, - space_below=space_below, - named_style_type=named_style_type, + heading_level, + alignment, + line_spacing, + indent_first_line, + indent_start, + indent_end, + space_above, + space_below, + named_style_type, ) if not paragraph_style: diff --git a/gdocs/managers/validation_manager.py b/gdocs/managers/validation_manager.py index 7b13c21..3235c1a 100644 --- a/gdocs/managers/validation_manager.py +++ b/gdocs/managers/validation_manager.py @@ -9,7 +9,7 @@ import logging from typing import Dict, Any, List, Tuple, Optional from urllib.parse import urlparse -from gdocs.docs_helpers import validate_operation, VALID_NAMED_STYLE_TYPES +from gdocs.docs_helpers import validate_operation logger = logging.getLogger(__name__) @@ -317,10 +317,15 @@ class ValidationManager: ) if named_style_type is not None: - if named_style_type not in VALID_NAMED_STYLE_TYPES: + valid_styles = [ + "NORMAL_TEXT", "TITLE", "SUBTITLE", + "HEADING_1", "HEADING_2", "HEADING_3", + "HEADING_4", "HEADING_5", "HEADING_6", + ] + if named_style_type not in valid_styles: return ( False, - f"Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(VALID_NAMED_STYLE_TYPES)}", + f"Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(valid_styles)}", ) if heading_level is not None: From 59b3a2492c42693ab942b9e3a6946a1e86f39bac Mon Sep 17 00:00:00 2001 From: Rein Lemmens Date: Thu, 12 Mar 2026 21:20:17 +0100 Subject: [PATCH 4/5] Reject heading_level and named_style_type together in validation Add mutual exclusion guard so clients get a clear error instead of named_style_type silently overriding heading_level. Co-Authored-By: Claude Opus 4.6 --- gdocs/managers/validation_manager.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gdocs/managers/validation_manager.py b/gdocs/managers/validation_manager.py index 3235c1a..d5917e7 100644 --- a/gdocs/managers/validation_manager.py +++ b/gdocs/managers/validation_manager.py @@ -316,6 +316,12 @@ class ValidationManager: "At least one paragraph style parameter must be provided (heading_level, alignment, line_spacing, indent_first_line, indent_start, indent_end, space_above, space_below, or named_style_type)", ) + if heading_level is not None and named_style_type is not None: + return ( + False, + "heading_level and named_style_type are mutually exclusive; provide only one", + ) + if named_style_type is not None: valid_styles = [ "NORMAL_TEXT", "TITLE", "SUBTITLE", From 2ab22ee6307ee63c2b22abbb1185b639a709baa1 Mon Sep 17 00:00:00 2001 From: Taylor Wilsdon Date: Mon, 16 Mar 2026 14:44:45 -0400 Subject: [PATCH 5/5] refac --- gdocs/docs_tools.py | 23 ++++++++++++++++++++++- gdocs/managers/batch_operation_manager.py | 1 + 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gdocs/docs_tools.py b/gdocs/docs_tools.py index c040c0b..3d7b316 100644 --- a/gdocs/docs_tools.py +++ b/gdocs/docs_tools.py @@ -1463,6 +1463,7 @@ async def update_paragraph_style( indent_end: float = None, space_above: float = None, space_below: float = None, + named_style_type: str = None, list_type: str = None, list_nesting_level: int = None, ) -> str: @@ -1488,6 +1489,8 @@ async def update_paragraph_style( indent_end: Right/end indent in points space_above: Space above paragraph in points (e.g., 12 for one line) space_below: Space below paragraph in points + named_style_type: Direct named style type - 'NORMAL_TEXT', 'TITLE', 'SUBTITLE', + 'HEADING_1' through 'HEADING_6'. Mutually exclusive with heading_level. list_type: Create a list from existing paragraphs ('UNORDERED' for bullets, 'ORDERED' for numbers) list_nesting_level: Nesting level for lists (0-8, where 0 is top level, default is 0) Use higher levels for nested/indented list items @@ -1546,12 +1549,30 @@ async def update_paragraph_style( if list_nesting_level < 0 or list_nesting_level > 8: return "Error: list_nesting_level must be between 0 and 8" + # Validate named_style_type + if named_style_type is not None and heading_level is not None: + return "Error: heading_level and named_style_type are mutually exclusive; provide only one" + + if named_style_type is not None: + valid_styles = [ + "NORMAL_TEXT", "TITLE", "SUBTITLE", + "HEADING_1", "HEADING_2", "HEADING_3", + "HEADING_4", "HEADING_5", "HEADING_6", + ] + if named_style_type not in valid_styles: + return f"Error: Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(valid_styles)}" + # Build paragraph style object paragraph_style = {} fields = [] + # Handle named_style_type (direct named style) + if named_style_type is not None: + paragraph_style["namedStyleType"] = named_style_type + fields.append("namedStyleType") + # Handle heading level (named style) - if heading_level is not None: + elif heading_level is not None: if heading_level < 0 or heading_level > 6: return "Error: heading_level must be between 0 (normal text) and 6" if heading_level == 0: diff --git a/gdocs/managers/batch_operation_manager.py b/gdocs/managers/batch_operation_manager.py index 97bd284..c0d5368 100644 --- a/gdocs/managers/batch_operation_manager.py +++ b/gdocs/managers/batch_operation_manager.py @@ -475,6 +475,7 @@ class BatchOperationManager: "indent_end", "space_above", "space_below", + "named_style_type", ], "description": "Apply paragraph-level styling (headings, alignment, spacing, indentation)", },