Refactor logging setup and usage for better event tracking

This commit is contained in:
Roger Gonzalez 2024-11-24 23:24:23 -03:00
parent 5511bb1dd0
commit 8df31ef074
Signed by: rogs
GPG Key ID: C7ECE9C6C36EC2E6
5 changed files with 201 additions and 75 deletions

View File

@ -8,6 +8,9 @@ from google.auth.credentials import Credentials
from google.auth.transport.requests import Request from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import Resource, build from googleapiclient.discovery import Resource, build
from logger import setup_logger
logger = setup_logger(__name__)
SCOPES: List[str] = ["https://www.googleapis.com/auth/calendar"] SCOPES: List[str] = ["https://www.googleapis.com/auth/calendar"]
@ -23,16 +26,20 @@ def authenticate_google() -> Resource:
""" """
creds: Credentials | None = None creds: Credentials | None = None
if os.path.exists("token.pickle"): if os.path.exists("token.pickle"):
logger.debug("Loading existing credentials from token.pickle")
with open("token.pickle", "rb") as token: with open("token.pickle", "rb") as token:
creds = pickle.load(token) creds = pickle.load(token)
if not creds or not creds.valid: if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token: if creds and creds.expired and creds.refresh_token:
logger.info("Refreshing expired credentials")
creds.refresh(Request()) creds.refresh(Request())
else: else:
logger.info("Starting new OAuth2 flow")
flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES) flow = InstalledAppFlow.from_client_secrets_file("credentials.json", SCOPES)
creds = flow.run_local_server(port=0) creds = flow.run_local_server(port=0)
logger.debug("Saving credentials to token.pickle")
with open("token.pickle", "wb") as token: with open("token.pickle", "wb") as token:
pickle.dump(creds, token) pickle.dump(creds, token)
@ -52,11 +59,16 @@ def search_calendar_id(service: Resource, calendar_name: str) -> str:
Raises: Raises:
ValueError: If the target calendar is not found. ValueError: If the target calendar is not found.
""" """
logger.info(f"Searching for calendar: {calendar_name}")
calendars_result = service.calendarList().list().execute() calendars_result = service.calendarList().list().execute()
calendars = calendars_result.get("items", []) calendars = calendars_result.get("items", [])
logger.debug(f"Found {len(calendars)} calendars in total")
for calendar in calendars: for calendar in calendars:
if calendar["summary"].lower() == calendar_name.lower(): if calendar["summary"].lower() == calendar_name.lower():
logger.info(f"Found matching calendar with ID: {calendar['id']}")
return calendar["id"] return calendar["id"]
raise ValueError(f"No calendar named '{calendar_name}' found.") error_msg = f"No calendar named '{calendar_name}' found"
logger.error(error_msg)
raise ValueError(error_msg)

View File

@ -5,6 +5,9 @@ from typing import Any, Dict
from caldav import Calendar as CalDAVCalendar from caldav import Calendar as CalDAVCalendar
from caldav import DAVClient, Principal from caldav import DAVClient, Principal
from icalendar import Calendar from icalendar import Calendar
from logger import setup_logger
logger = setup_logger(__name__)
EventDict = Dict[str, Any] EventDict = Dict[str, Any]
EventsDict = Dict[str, EventDict] EventsDict = Dict[str, EventDict]
@ -21,8 +24,11 @@ def connect_to_caldav(url: str, username: str, password: str) -> Principal:
Returns: Returns:
Principal: The authenticated CalDAV principal object. Principal: The authenticated CalDAV principal object.
""" """
logger.info(f"Connecting to CalDAV server at {url}")
client = DAVClient(url, username=username, password=password) client = DAVClient(url, username=username, password=password)
return client.principal() principal = client.principal()
logger.info("Successfully connected to CalDAV server")
return principal
def get_calendar(principal: Principal, calendar_name: str) -> CalDAVCalendar: def get_calendar(principal: Principal, calendar_name: str) -> CalDAVCalendar:
@ -38,15 +44,22 @@ def get_calendar(principal: Principal, calendar_name: str) -> CalDAVCalendar:
Raises: Raises:
ValueError: If no matching calendar is found. ValueError: If no matching calendar is found.
""" """
logger.info("Retrieving list of calendars")
calendars = principal.calendars() calendars = principal.calendars()
if not calendars: if not calendars:
raise ValueError("No calendars found on the server.") error_msg = "No calendars found on the server"
logger.error(error_msg)
raise ValueError(error_msg)
logger.debug(f"Found {len(calendars)} calendars")
for cal in calendars: for cal in calendars:
if cal.name.lower() == calendar_name.lower(): if cal.name.lower() == calendar_name.lower():
logger.info(f"Found matching calendar: {cal.name}")
return cal return cal
raise ValueError(f"No calendar named '{calendar_name}' found.") error_msg = f"No calendar named '{calendar_name}' found"
logger.error(error_msg)
raise ValueError(error_msg)
def _process_exdate(exdate: Any) -> list: def _process_exdate(exdate: Any) -> list:
@ -87,11 +100,16 @@ def fetch_events(calendar: CalDAVCalendar) -> EventsDict:
Returns: Returns:
EventsDict: Dictionary of events indexed by their UIDs. EventsDict: Dictionary of events indexed by their UIDs.
""" """
logger.info(f"Fetching events from calendar: {calendar.name}")
events: EventsDict = {} events: EventsDict = {}
event_count = 0
recurrence_count = 0
for event in calendar.events(): for event in calendar.events():
ical = Calendar.from_ical(event.data) ical = Calendar.from_ical(event.data)
for component in ical.walk(): for component in ical.walk():
if component.name == "VEVENT": if component.name == "VEVENT":
event_count += 1
uid = str(component.get("UID")) uid = str(component.get("UID"))
dtstart = component.get("DTSTART") dtstart = component.get("DTSTART")
dtend = component.get("DTEND") dtend = component.get("DTEND")
@ -102,6 +120,7 @@ def fetch_events(calendar: CalDAVCalendar) -> EventsDict:
recurrence_id = component.get("RECURRENCE-ID") recurrence_id = component.get("RECURRENCE-ID")
if recurrence_id: if recurrence_id:
uid = f"{uid}-{recurrence_id.dt.isoformat()}" uid = f"{uid}-{recurrence_id.dt.isoformat()}"
recurrence_count += 1
description = component.get("DESCRIPTION") description = component.get("DESCRIPTION")
description = str(description) if description else "" description = str(description) if description else ""
@ -123,4 +142,5 @@ def fetch_events(calendar: CalDAVCalendar) -> EventsDict:
"google_event_id": None, "google_event_id": None,
} }
logger.info(f"Retrieved {event_count} events ({recurrence_count} recurring instances) from CalDAV calendar")
return events return events

30
src/logger.py Normal file
View File

@ -0,0 +1,30 @@
"""Module for setting up logging configuration."""
import logging
# Configure logging format
LOGFORMAT = "%(asctime)s - %(levelname)s - %(message)s"
DATEFORMAT = "%Y-%m-%d %H:%M:%S"
def setup_logger(name: str) -> logging.Logger:
"""Set up and return a logger instance with console output.
Args:
name: Name of the logger, typically __name__ from the calling module
Returns:
logging.Logger: Configured logger instance
"""
logger = logging.getLogger(name)
if not logger.handlers:
logger.setLevel(logging.INFO)
formatter = logging.Formatter(LOGFORMAT, DATEFORMAT)
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(console_handler)
return logger

View File

@ -5,6 +5,7 @@ import os
from auth_google import authenticate_google, search_calendar_id from auth_google import authenticate_google, search_calendar_id
from caldav_client import connect_to_caldav, fetch_events, get_calendar from caldav_client import connect_to_caldav, fetch_events, get_calendar
from dotenv import load_dotenv from dotenv import load_dotenv
from logger import setup_logger
from sync_logic import ( from sync_logic import (
add_event_to_google, add_event_to_google,
compare_events, compare_events,
@ -14,6 +15,8 @@ from sync_logic import (
save_local_sync, save_local_sync,
) )
logger = setup_logger(__name__)
load_dotenv() load_dotenv()
LOCAL_SYNC_FILE = "calendar_sync.json" LOCAL_SYNC_FILE = "calendar_sync.json"
@ -27,43 +30,50 @@ GOOGLE_CALENDAR_NAME = os.getenv("GOOGLE_CALENDAR_NAME")
def main() -> None: def main() -> None:
"""Run the calendar synchronization process.""" """Run the calendar synchronization process."""
try: try:
print("Authenticating with Google Calendar...") logger.info("Starting calendar synchronization process")
logger.info("Authenticating with Google Calendar...")
service = authenticate_google() service = authenticate_google()
google_calendar_id = search_calendar_id(service, GOOGLE_CALENDAR_NAME) google_calendar_id = search_calendar_id(service, GOOGLE_CALENDAR_NAME)
logger.info("Successfully authenticated with Google Calendar")
print("Connecting to CalDAV server...") logger.info("Connecting to CalDAV server...")
principal = connect_to_caldav(CALDAV_URL, CALDAV_USERNAME, CALDAV_PASSWORD) principal = connect_to_caldav(CALDAV_URL, CALDAV_USERNAME, CALDAV_PASSWORD)
caldav_calendar = get_calendar(principal, CALDAV_CALENDAR_NAME) caldav_calendar = get_calendar(principal, CALDAV_CALENDAR_NAME)
logger.info("Successfully connected to CalDAV server")
print(f"Fetching events from CalDAV calendar: {caldav_calendar.name}") logger.info(f"Fetching events from CalDAV calendar: {caldav_calendar.name}")
server_events = fetch_events(caldav_calendar) server_events = fetch_events(caldav_calendar)
logger.info(f"Retrieved {len(server_events)} events from CalDAV")
print("Loading local sync data...") logger.info("Loading local sync data...")
local_events = load_local_sync(LOCAL_SYNC_FILE) local_events = load_local_sync(LOCAL_SYNC_FILE)
logger.info(f"Loaded {len(local_events)} events from local sync file")
print("Comparing events...") logger.info("Comparing events...")
new_events, updated_events, deleted_events = compare_events(local_events, server_events) new_events, updated_events, deleted_events = compare_events(local_events, server_events)
print(f"Adding/Updating {len(new_events) + len(updated_events)} events to Google Calendar...") logger.info(f"Adding {len(new_events)} new events and updating {len(updated_events)} events in Google Calendar")
for event in new_events + updated_events: for event in new_events + updated_events:
add_event_to_google(service, event, google_calendar_id) add_event_to_google(service, event, google_calendar_id)
print(f"Deleting {len(deleted_events)} events from Google Calendar...") logger.info(f"Deleting {len(deleted_events)} events from Google Calendar")
for event in deleted_events: for event in deleted_events:
delete_event_from_google(service, event, google_calendar_id) delete_event_from_google(service, event, google_calendar_id)
print("Saving updated sync data...") logger.info("Saving updated sync data...")
save_local_sync(LOCAL_SYNC_FILE, server_events) save_local_sync(LOCAL_SYNC_FILE, server_events)
print("Sync process completed successfully.") logger.info("Sync process completed successfully")
if error_events: if error_events:
print("The following events encountered errors during sync:") logger.warning("The following events encountered errors during sync:")
for event in error_events: for event in error_events:
print(event) logger.warning(f"Failed event: {event['summary']} (UID: {event['uid']})")
except Exception as e: except Exception as e:
print(f"Error occurred during sync: {e}") logger.error(f"Error occurred during sync: {str(e)}", exc_info=True)
raise
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -7,11 +7,14 @@ from datetime import datetime
from typing import Any, Dict, List, Tuple from typing import Any, Dict, List, Tuple
from googleapiclient.discovery import Resource from googleapiclient.discovery import Resource
from logger import setup_logger
logger = setup_logger(__name__)
EventDict = Dict[str, Any] EventDict = Dict[str, Any]
EventsDict = Dict[str, EventDict] EventsDict = Dict[str, EventDict]
error_events = [] error_events: List[EventDict] = []
def _sanitize_event_for_json(event_data: Dict[str, Any]) -> Dict[str, Any]: def _sanitize_event_for_json(event_data: Dict[str, Any]) -> Dict[str, Any]:
@ -22,9 +25,6 @@ def _sanitize_event_for_json(event_data: Dict[str, Any]) -> Dict[str, Any]:
Returns: Returns:
Dict[str, Any]: Sanitized dictionary. Dict[str, Any]: Sanitized dictionary.
Raises:
TypeError: If the event data is not JSON serializable.
""" """
sanitized = event_data.copy() sanitized = event_data.copy()
@ -38,53 +38,6 @@ def _sanitize_event_for_json(event_data: Dict[str, Any]) -> Dict[str, Any]:
return sanitized return sanitized
def load_local_sync(file_path: str) -> EventsDict:
"""Load the locally synced events from a JSON file.
Args:
file_path: Path to the JSON file.
Returns:
EventsDict: Dictionary of previously synced events.
"""
if not os.path.exists(file_path):
return {}
with open(file_path, "r") as file:
return json.load(file)
def save_local_sync(file_path: str, events: EventsDict) -> None:
"""Save the events to the local sync JSON file.
Args:
file_path: Path to the JSON file.
events: Dictionary of events to save.
"""
sanitized_events = {}
for event_id, event_data in events.items():
try:
sanitized_events[event_id] = _sanitize_event_for_json(event_data)
except Exception as e:
print(f"Warning: Could not sanitize event {event_id}: {e}")
continue
try:
with open(file_path, "w") as file:
json.dump(sanitized_events, file, indent=4)
except TypeError as e:
print(f"Error saving events: {e}")
for event_id, event_data in sanitized_events.items():
try:
json.dumps(event_data)
except TypeError as e:
print(f"Problem found in event {event_id}: {e}")
for key, value in event_data.items():
try:
json.dumps({key: value})
except TypeError as e:
print(f" Problem field: {key} = {value} (type: {type(value)})")
def compare_events( def compare_events(
local_events: EventsDict, local_events: EventsDict,
server_events: EventsDict, server_events: EventsDict,
@ -102,20 +55,101 @@ def compare_events(
updated_events: List[EventDict] = [] updated_events: List[EventDict] = []
deleted_events: List[EventDict] = [] deleted_events: List[EventDict] = []
logger.info(f"Comparing {len(server_events)} server events with {len(local_events)} local events")
# Find new and updated events
for uid, event in server_events.items(): for uid, event in server_events.items():
if uid not in local_events: if uid not in local_events:
logger.debug(f"New event found: {event['summary']} (UID: {uid})")
new_events.append(event) new_events.append(event)
elif event["last_modified"] != local_events[uid].get("last_modified"): elif event["last_modified"] != local_events[uid].get("last_modified"):
logger.debug(f"Modified event found: {event['summary']} (UID: {uid})")
event["google_event_id"] = local_events[uid].get("google_event_id") event["google_event_id"] = local_events[uid].get("google_event_id")
updated_events.append(event) updated_events.append(event)
for uid in local_events: # Find deleted events
for uid, event in local_events.items():
if uid not in server_events: if uid not in server_events:
deleted_events.append(local_events[uid]) logger.debug(f"Deleted event found: {event['summary']} (UID: {uid})")
deleted_events.append(event)
logger.info(
f"Found {len(new_events)} new events, {len(updated_events)} modified events, "
f"and {len(deleted_events)} deleted events",
)
return new_events, updated_events, deleted_events return new_events, updated_events, deleted_events
def load_local_sync(file_path: str) -> EventsDict:
"""Load the locally synced events from a JSON file.
Args:
file_path: Path to the JSON file.
Returns:
EventsDict: Dictionary of previously synced events.
"""
logger.info(f"Loading local sync data from {file_path}")
if not os.path.exists(file_path):
logger.info("No existing sync file found, starting fresh")
return {}
try:
with open(file_path, "r") as file:
events = json.load(file)
logger.info(f"Successfully loaded {len(events)} events from local sync file")
return events
except json.JSONDecodeError as e:
logger.error(f"Error decoding JSON from {file_path}: {str(e)}")
return {}
except Exception as e:
logger.error(f"Unexpected error loading sync file: {str(e)}")
return {}
def save_local_sync(file_path: str, events: EventsDict) -> None:
"""Save the events to the local sync JSON file.
Args:
file_path: Path to the JSON file.
events: Dictionary of events to save.
"""
logger.info(f"Saving {len(events)} events to local sync file")
sanitized_events = {}
# Sanitize events
for event_id, event_data in events.items():
try:
sanitized_events[event_id] = _sanitize_event_for_json(event_data)
except Exception as e:
logger.error(f"Failed to sanitize event {event_id} ({event_data.get('summary', 'No summary')}): {str(e)}")
continue
# Save to file
try:
with open(file_path, "w") as file:
json.dump(sanitized_events, file, indent=4)
logger.info(f"Successfully saved {len(sanitized_events)} events to {file_path}")
except Exception as e:
logger.error(f"Failed to save sync file: {str(e)}")
logger.debug("Attempting to identify problematic events...")
for event_id, event_data in sanitized_events.items():
try:
json.dumps(event_data)
except TypeError as e:
logger.error(f"JSON serialization failed for event: {event_id}")
logger.error(f"Event summary: {event_data.get('summary', 'No summary')}")
logger.error(f"Error: {str(e)}")
# Debug field values
for key, value in event_data.items():
try:
json.dumps({key: value})
except TypeError:
logger.error(f"Problematic field: {key} = {value} (type: {type(value)})")
def add_event_to_google(service: Resource, event: EventDict, calendar_id: str) -> None: def add_event_to_google(service: Resource, event: EventDict, calendar_id: str) -> None:
"""Add a single event to Google Calendar. """Add a single event to Google Calendar.
@ -124,8 +158,10 @@ def add_event_to_google(service: Resource, event: EventDict, calendar_id: str) -
event: Dictionary containing event details. event: Dictionary containing event details.
calendar_id: ID of the target Google Calendar. calendar_id: ID of the target Google Calendar.
""" """
logger.info(f"Processing event: {event['summary']} (UID: {event['uid']})")
try: try:
print(f"Adding event to Google Calendar: {event['summary']}") # Prepare event data
google_event = { google_event = {
"summary": event["summary"], "summary": event["summary"],
"description": event.get("description", ""), "description": event.get("description", ""),
@ -134,7 +170,9 @@ def add_event_to_google(service: Resource, event: EventDict, calendar_id: str) -
"end": {"dateTime": event["end"], "timeZone": "UTC"}, "end": {"dateTime": event["end"], "timeZone": "UTC"},
} }
# Handle recurring events
if event.get("rrule"): if event.get("rrule"):
logger.debug(f"Processing recurring event rules for {event['summary']}")
rrule_parts = [] rrule_parts = []
for key, value in event["rrule"].items(): for key, value in event["rrule"].items():
if isinstance(value, list): if isinstance(value, list):
@ -143,10 +181,13 @@ def add_event_to_google(service: Resource, event: EventDict, calendar_id: str) -
rrule_parts.append(f"{key}={value}") rrule_parts.append(f"{key}={value}")
google_event["recurrence"] = [f"RRULE:{';'.join(rrule_parts)}"] google_event["recurrence"] = [f"RRULE:{';'.join(rrule_parts)}"]
# Handle excluded dates
if event.get("exdate"): if event.get("exdate"):
logger.debug(f"Processing {len(event['exdate'])} excluded dates")
exdates = [f"EXDATE;TZID=UTC:{date}" for date in event["exdate"]] exdates = [f"EXDATE;TZID=UTC:{date}" for date in event["exdate"]]
google_event["recurrence"].extend(exdates) google_event["recurrence"].extend(exdates)
# Add event to Google Calendar
created_event = ( created_event = (
service.events() service.events()
.insert( .insert(
@ -157,10 +198,14 @@ def add_event_to_google(service: Resource, event: EventDict, calendar_id: str) -
) )
event["google_event_id"] = created_event["id"] event["google_event_id"] = created_event["id"]
print(f"Event created: {created_event.get('htmlLink')}") logger.info(f"Successfully created event: {event['summary']} (Google ID: {created_event['id']})")
# Rate limiting
time.sleep(0.5) time.sleep(0.5)
except Exception as e: except Exception as e:
print(f"Failed to add event: {event['summary']} - {e}") logger.error(f"Failed to add event {event['summary']} (UID: {event['uid']})")
logger.error(f"Error: {str(e)}")
error_events.append(event) error_events.append(event)
@ -174,9 +219,18 @@ def delete_event_from_google(service: Resource, event: EventDict, calendar_id: s
""" """
google_event_id = event.get("google_event_id") google_event_id = event.get("google_event_id")
if not google_event_id: if not google_event_id:
raise ValueError(f"Cannot delete event {event['summary']}: missing Google Calendar ID") error_msg = f"Cannot delete event {event['summary']} (UID: {event['uid']}): missing Google Calendar ID"
logger.error(error_msg)
raise ValueError(error_msg)
try: try:
logger.info(f"Deleting event: {event['summary']} (Google ID: {google_event_id})")
service.events().delete(calendarId=calendar_id, eventId=google_event_id).execute() service.events().delete(calendarId=calendar_id, eventId=google_event_id).execute()
print(f"Deleted event: {event['summary']}") logger.info(f"Successfully deleted event: {event['summary']}")
# Rate limiting
time.sleep(0.5)
except Exception as e: except Exception as e:
print(f"Failed to delete event: {event['summary']} - {e}") logger.error(f"Failed to delete event: {event['summary']} (UID: {event['uid']})")
logger.error(f"Error: {str(e)}")