From c39e374af4f09a0ec6ddbf9b3410ff60b6777055 Mon Sep 17 00:00:00 2001 From: Mathieu Virbel Date: Thu, 11 Sep 2025 10:06:01 -0600 Subject: [PATCH] fix: resolve pyflakes warnings in ics_sync and meetings modules Remove unused imports and variables to clean up code quality --- server/reflector/services/ics_sync.py | 96 +++++++++++++++++---------- server/reflector/views/meetings.py | 18 ++--- 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/server/reflector/services/ics_sync.py b/server/reflector/services/ics_sync.py index 997ebd52..d4b5a35b 100644 --- a/server/reflector/services/ics_sync.py +++ b/server/reflector/services/ics_sync.py @@ -1,3 +1,51 @@ +""" +ICS Calendar Synchronization Service + +This module provides services for fetching, parsing, and synchronizing ICS (iCalendar) +calendar feeds with room booking data in the database. + +Key Components: +- ICSFetchService: Handles HTTP fetching and parsing of ICS calendar data +- ICSSyncService: Manages the synchronization process between ICS feeds and database + +Example Usage: + # Sync a room's calendar + room = Room(id="room1", name="conference-room", ics_url="https://cal.example.com/room.ics") + result = await ics_sync_service.sync_room_calendar(room) + + # Result structure: + { + "status": "success", # success|unchanged|error|skipped + "hash": "abc123...", # MD5 hash of ICS content + "events_found": 5, # Events matching this room + "total_events": 12, # Total events in calendar within time window + "events_created": 2, # New events added to database + "events_updated": 3, # Existing events modified + "events_deleted": 1 # Events soft-deleted (no longer in calendar) + } + +Event Matching: + Events are matched to rooms by checking if the room's full URL appears in the + event's LOCATION or DESCRIPTION fields. Only events within a 25-hour window + (1 hour ago to 24 hours from now) are processed. + +Input: ICS calendar URL (e.g., "https://calendar.google.com/calendar/ical/...") +Output: EventData objects with structured calendar information: + { + "ics_uid": "event123@google.com", + "title": "Team Meeting", + "description": "Weekly sync meeting", + "location": "https://meet.company.com/conference-room", + "start_time": datetime(2024, 1, 15, 14, 0, tzinfo=UTC), + "end_time": datetime(2024, 1, 15, 15, 0, tzinfo=UTC), + "attendees": [ + {"email": "user@company.com", "name": "John Doe", "role": "ORGANIZER"}, + {"email": "attendee@company.com", "name": "Jane Smith", "status": "ACCEPTED"} + ], + "ics_raw_data": "BEGIN:VEVENT\nUID:event123@google.com\n..." + } +""" + import hashlib from datetime import date, datetime, timedelta, timezone from enum import Enum @@ -14,6 +62,9 @@ from reflector.settings import settings logger = structlog.get_logger() +EVENT_WINDOW_DELTA_START = timedelta(hours=-1) +EVENT_WINDOW_DELTA_END = timedelta(hours=24) + class SyncStatus(str, Enum): SUCCESS = "success" @@ -82,8 +133,8 @@ class ICSFetchService: events = [] total_events = 0 now = datetime.now(timezone.utc) - window_start = now - timedelta(hours=1) - window_end = now + timedelta(hours=24) + window_start = now + timedelta(hours=EVENT_WINDOW_DELTA_START) + window_end = now + timedelta(hours=EVENT_WINDOW_DELTA_END) for component in calendar.walk(): if component.name != "VEVENT": @@ -116,7 +167,6 @@ class ICSFetchService: # Check location and description for patterns text_to_check = f"{location} {description}".lower() - for pattern in patterns: if pattern.lower() in text_to_check: return True @@ -124,20 +174,17 @@ class ICSFetchService: return False def _parse_event(self, event: Event) -> EventData | None: - # Extract basic fields uid = str(event.get("UID", "")) summary = str(event.get("SUMMARY", "")) description = str(event.get("DESCRIPTION", "")) location = str(event.get("LOCATION", "")) - - # Parse dates dtstart = event.get("DTSTART") dtend = event.get("DTEND") if not dtstart: return None - # Convert to datetime + # Convert fields start_time = self._normalize_datetime( dtstart.dt if hasattr(dtstart, "dt") else dtstart ) @@ -146,8 +193,6 @@ class ICSFetchService: if dtend else start_time + timedelta(hours=1) ) - - # Parse attendees attendees = self._parse_attendees(event) # Get raw event data for storage @@ -165,25 +210,25 @@ class ICSFetchService: } def _normalize_datetime(self, dt) -> datetime: - # Handle date objects (all-day events) + # Ensure datetime is with timezone, if not, assume UTC if isinstance(dt, date) and not isinstance(dt, datetime): - # Convert to datetime at start of day in UTC dt = datetime.combine(dt, datetime.min.time()) dt = pytz.UTC.localize(dt) elif isinstance(dt, datetime): - # Add UTC timezone if naive if dt.tzinfo is None: dt = pytz.UTC.localize(dt) else: - # Convert to UTC dt = dt.astimezone(pytz.UTC) return dt def _parse_attendees(self, event: Event) -> list[AttendeeData]: + # Extracts attendee information from both ATTENDEE and ORGANIZER properties. + # Handles malformed comma-separated email addresses in single ATTENDEE fields + # by splitting them into separate attendee entries. Returns a list of attendee + # data including email, name, status, and role information. final_attendees = [] - # Parse ATTENDEE properties attendees = event.get("ATTENDEE", []) if not isinstance(attendees, list): attendees = [attendees] @@ -195,8 +240,7 @@ class ICSFetchService: # Split comma-separated emails and create separate attendee entries email_parts = [email.strip() for email in email_str.split(",")] for email in email_parts: - if email and "@" in email: # Basic email validation - # Clean up any remaining MAILTO: prefix + if email and "@" in email: clean_email = email.replace("MAILTO:", "").replace( "mailto:", "" ) @@ -254,19 +298,15 @@ class ICSSyncService: return {"status": SyncStatus.SKIPPED, "reason": "ICS not configured"} try: - # Check if it's time to sync if not self._should_sync(room): return {"status": SyncStatus.SKIPPED, "reason": "Not time to sync yet"} - # Fetch ICS file ics_content = await self.fetch_service.fetch_ics(room.ics_url) + calendar = self.fetch_service.parse_ics(ics_content) - # Check if content changed content_hash = hashlib.md5(ics_content.encode()).hexdigest() if room.ics_last_etag == content_hash: logger.info("No changes in ICS for room", room_id=room.id) - # Still parse to get event count - calendar = self.fetch_service.parse_ics(ics_content) room_url = f"{settings.UI_BASE_URL}/{room.name}" events, total_events = self.fetch_service.extract_room_events( calendar, room.name, room_url @@ -281,18 +321,11 @@ class ICSSyncService: "events_deleted": 0, } - # Parse calendar - calendar = self.fetch_service.parse_ics(ics_content) - - # Build room URL - room_url = f"{settings.UI_BASE_URL}/{room.name}" - # Extract matching events + room_url = f"{settings.UI_BASE_URL}/{room.name}" events, total_events = self.fetch_service.extract_room_events( calendar, room.name, room_url ) - - # Sync events to database sync_result = await self._sync_events_to_database(room.id, events) # Update room sync metadata @@ -330,14 +363,10 @@ class ICSSyncService: created = 0 updated = 0 - # Track current event IDs current_ics_uids = [] for event_data in events: - # Create CalendarEvent object calendar_event = CalendarEvent(room_id=room_id, **event_data) - - # Upsert event existing = await calendar_events_controller.get_by_ics_uid( room_id, event_data["ics_uid"] ) @@ -362,5 +391,4 @@ class ICSSyncService: } -# Global instance ics_sync_service = ICSSyncService() diff --git a/server/reflector/views/meetings.py b/server/reflector/views/meetings.py index 96afb7fa..25987e47 100644 --- a/server/reflector/views/meetings.py +++ b/server/reflector/views/meetings.py @@ -47,33 +47,29 @@ async def meeting_audio_consent( @router.patch("/meetings/{meeting_id}/deactivate") async def meeting_deactivate( meeting_id: str, - user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user)], ): - """Deactivate a meeting (owner only)""" + user_id = user["sub"] if user else None + if not user_id: + raise HTTPException(status_code=401, detail="Authentication required") + meeting = await meetings_controller.get_by_id(meeting_id) if not meeting: raise HTTPException(status_code=404, detail="Meeting not found") if not meeting.is_active: - raise HTTPException(status_code=400, detail="Meeting is already inactive") + return {"status": "success", "meeting_id": meeting_id} - # Check if user is the meeting owner or room owner - user_id = user["sub"] if user else None - if not user_id: - raise HTTPException(status_code=401, detail="Authentication required") - - # Get room to check ownership + # Only room owner or meeting creator can deactivate room = await rooms_controller.get_by_id(meeting.room_id) if not room: raise HTTPException(status_code=404, detail="Room not found") - # Only room owner or meeting creator can deactivate if user_id != room.user_id and user_id != meeting.user_id: raise HTTPException( status_code=403, detail="Only the room owner can deactivate meetings" ) - # Deactivate the meeting await meetings_controller.update_meeting(meeting_id, is_active=False) return {"status": "success", "meeting_id": meeting_id}