Simple API for Google Calendar management
—
Checking calendar availability, free/busy time queries, and scheduling conflict detection. The FreeBusy class provides tools for determining when calendars and users are available for scheduling.
from gcsa.free_busy import FreeBusy, TimeRange, FreeBusyQueryError
from datetime import datetime, timedeltaRepresents free/busy information for given calendar(s) and/or group(s), providing availability data for scheduling decisions.
class FreeBusy:
def __init__(
self,
time_min,
time_max,
groups: dict = None,
calendars: dict = None,
groups_errors: dict = None,
calendars_errors: dict = None
):
"""
Free/busy information for calendars and groups.
Parameters:
- time_min (datetime): Lower bound for the query
- time_max (datetime): Upper bound for the query
- groups (dict): Free/busy data for groups (group_id -> busy_times)
- calendars (dict): Free/busy data for calendars (calendar_id -> busy_times)
- groups_errors (dict): Errors for group queries
- calendars_errors (dict): Errors for calendar queries
"""
def __iter__(self):
"""
Iterate over busy time ranges for single calendar queries.
Returns:
Iterator of TimeRange objects
"""Named tuple representing a time range with start and end times.
class TimeRange:
"""
Represents a time range with start and end times.
Attributes:
- start (datetime): Start time of the range
- end (datetime): End time of the range
"""
start: datetime
end: datetimeException class for free/busy query errors.
class FreeBusyQueryError(Exception):
def __init__(
self,
groups_errors: dict = None,
calendars_errors: dict = None
):
"""
Exception for free/busy query errors.
Parameters:
- groups_errors (dict): Errors for group queries
- calendars_errors (dict): Errors for calendar queries
"""Method for querying free/busy information through the main GoogleCalendar client.
def get_free_busy(
self,
time_min,
time_max,
calendars: list = None,
groups: list = None,
group_expansion_max: int = None,
calendar_expansion_max: int = None
):
"""
Get free/busy information for calendars and groups.
Parameters:
- time_min (datetime): Lower bound for query (inclusive)
- time_max (datetime): Upper bound for query (exclusive)
- calendars (list): List of calendar IDs to query
- groups (list): List of group IDs to query
- group_expansion_max (int): Maximum number of calendars to expand per group
- calendar_expansion_max (int): Maximum number of calendars to expand total
Returns:
FreeBusy object with availability information
"""from gcsa.google_calendar import GoogleCalendar
from datetime import datetime, timedelta
gc = GoogleCalendar()
# Query free/busy for next week
start_time = datetime.now()
end_time = start_time + timedelta(days=7)
# Check single calendar availability
free_busy = gc.get_free_busy(
time_min=start_time,
time_max=end_time,
calendars=['primary']
)
# Print busy times
print("Busy times:")
for busy_time in free_busy:
print(f" {busy_time.start} - {busy_time.end}")# Check multiple calendars
calendar_ids = [
'primary',
'work.calendar@company.com',
'personal@gmail.com'
]
free_busy = gc.get_free_busy(
time_min=datetime(2024, 1, 15, 0, 0),
time_max=datetime(2024, 1, 22, 0, 0),
calendars=calendar_ids
)
# Check each calendar's availability
if free_busy.calendars:
for calendar_id, busy_times in free_busy.calendars.items():
print(f"\nCalendar {calendar_id}:")
if busy_times:
for time_range in busy_times:
print(f" Busy: {time_range.start} - {time_range.end}")
else:
print(" No busy times")# Query group availability
group_ids = ['team@company.com', 'managers@company.com']
free_busy = gc.get_free_busy(
time_min=datetime(2024, 1, 15, 9, 0),
time_max=datetime(2024, 1, 15, 17, 0),
groups=group_ids,
group_expansion_max=10 # Limit calendars per group
)
# Check group availability
if free_busy.groups:
for group_id, busy_times in free_busy.groups.items():
print(f"\nGroup {group_id}:")
for time_range in busy_times:
print(f" Busy: {time_range.start} - {time_range.end}")def find_available_slots(gc, calendar_ids, start_time, end_time, slot_duration):
"""
Find available time slots for scheduling.
Parameters:
- gc: GoogleCalendar instance
- calendar_ids: List of calendar IDs to check
- start_time: Start of search period
- end_time: End of search period
- slot_duration: Required duration for slots (timedelta)
Returns:
List of available TimeRange objects
"""
# Get free/busy information
free_busy = gc.get_free_busy(
time_min=start_time,
time_max=end_time,
calendars=calendar_ids
)
# Collect all busy times
all_busy_times = []
if free_busy.calendars:
for calendar_id, busy_times in free_busy.calendars.items():
all_busy_times.extend(busy_times)
# Sort busy times by start time
all_busy_times.sort(key=lambda x: x.start)
# Find gaps between busy times
available_slots = []
current_time = start_time
for busy_time in all_busy_times:
# Check if there's a gap before this busy time
if current_time < busy_time.start:
gap_duration = busy_time.start - current_time
if gap_duration >= slot_duration:
available_slots.append(TimeRange(current_time, busy_time.start))
# Move current time to end of busy period
current_time = max(current_time, busy_time.end)
# Check for availability after last busy time
if current_time < end_time:
gap_duration = end_time - current_time
if gap_duration >= slot_duration:
available_slots.append(TimeRange(current_time, end_time))
return available_slots
# Example usage
calendar_ids = ['primary', 'colleague@company.com']
start_search = datetime(2024, 1, 15, 9, 0)
end_search = datetime(2024, 1, 15, 17, 0)
meeting_duration = timedelta(hours=1)
available_slots = find_available_slots(
gc, calendar_ids, start_search, end_search, meeting_duration
)
print("Available 1-hour slots:")
for slot in available_slots:
print(f" {slot.start} - {slot.end}")def analyze_team_availability(gc, team_calendars, start_date, end_date):
"""
Analyze when team members are generally available.
"""
# Query free/busy for the team
free_busy = gc.get_free_busy(
time_min=start_date,
time_max=end_date,
calendars=team_calendars
)
availability_report = {}
if free_busy.calendars:
for calendar_id, busy_times in free_busy.calendars.items():
total_period = end_date - start_date
total_busy_time = timedelta()
for busy_time in busy_times:
duration = busy_time.end - busy_time.start
total_busy_time += duration
# Calculate availability percentage
busy_percentage = (total_busy_time.total_seconds() /
total_period.total_seconds()) * 100
available_percentage = 100 - busy_percentage
availability_report[calendar_id] = {
'busy_time': total_busy_time,
'available_percentage': available_percentage,
'busy_periods': len(busy_times)
}
return availability_report
# Example usage
team_calendars = [
'alice@company.com',
'bob@company.com',
'charlie@company.com'
]
report = analyze_team_availability(
gc,
team_calendars,
datetime(2024, 1, 15, 0, 0),
datetime(2024, 1, 22, 0, 0)
)
for member, stats in report.items():
print(f"{member}:")
print(f" Available: {stats['available_percentage']:.1f}%")
print(f" Busy periods: {stats['busy_periods']}")def check_scheduling_conflicts(gc, calendar_ids, proposed_start, proposed_end):
"""
Check if a proposed meeting time conflicts with existing events.
"""
# Add buffer time for checking conflicts
buffer = timedelta(minutes=15)
check_start = proposed_start - buffer
check_end = proposed_end + buffer
free_busy = gc.get_free_busy(
time_min=check_start,
time_max=check_end,
calendars=calendar_ids
)
conflicts = {}
if free_busy.calendars:
for calendar_id, busy_times in free_busy.calendars.items():
calendar_conflicts = []
for busy_time in busy_times:
# Check if busy time overlaps with proposed time
if (busy_time.start < proposed_end and
busy_time.end > proposed_start):
calendar_conflicts.append(busy_time)
if calendar_conflicts:
conflicts[calendar_id] = calendar_conflicts
return conflicts
# Example usage
proposed_meeting_start = datetime(2024, 1, 15, 14, 0)
proposed_meeting_end = datetime(2024, 1, 15, 15, 0)
conflicts = check_scheduling_conflicts(
gc,
['primary', 'assistant@company.com'],
proposed_meeting_start,
proposed_meeting_end
)
if conflicts:
print("Scheduling conflicts detected:")
for calendar_id, conflict_times in conflicts.items():
print(f" {calendar_id}:")
for conflict in conflict_times:
print(f" {conflict.start} - {conflict.end}")
else:
print("No conflicts detected - time slot is available")from gcsa.free_busy import FreeBusyQueryError
try:
free_busy = gc.get_free_busy(
time_min=datetime(2024, 1, 15, 0, 0),
time_max=datetime(2024, 1, 22, 0, 0),
calendars=['invalid_calendar_id'],
groups=['invalid_group_id']
)
# Check for errors in the response
if free_busy.calendars_errors:
print("Calendar errors:")
for calendar_id, error in free_busy.calendars_errors.items():
print(f" {calendar_id}: {error}")
if free_busy.groups_errors:
print("Group errors:")
for group_id, error in free_busy.groups_errors.items():
print(f" {group_id}: {error}")
except FreeBusyQueryError as e:
print(f"Free/busy query failed: {e}")
except Exception as e:
print(f"Unexpected error: {e}")def get_daily_availability_pattern(gc, calendar_id, start_date, num_days=7):
"""
Analyze daily availability patterns over a period.
"""
daily_patterns = {}
for day_offset in range(num_days):
current_date = start_date + timedelta(days=day_offset)
day_start = current_date.replace(hour=9, minute=0, second=0, microsecond=0)
day_end = current_date.replace(hour=17, minute=0, second=0, microsecond=0)
free_busy = gc.get_free_busy(
time_min=day_start,
time_max=day_end,
calendars=[calendar_id]
)
# Calculate busy time for this day
busy_minutes = 0
if free_busy.calendars and calendar_id in free_busy.calendars:
for busy_time in free_busy.calendars[calendar_id]:
# Clamp to business hours
start = max(busy_time.start, day_start)
end = min(busy_time.end, day_end)
if start < end:
busy_minutes += (end - start).total_seconds() / 60
# 8-hour workday = 480 minutes
availability_percentage = (480 - busy_minutes) / 480 * 100
daily_patterns[current_date.strftime('%Y-%m-%d')] = {
'busy_minutes': busy_minutes,
'availability_percentage': availability_percentage
}
return daily_patterns
# Example usage
patterns = get_daily_availability_pattern(
gc,
'primary',
datetime(2024, 1, 15)
)
for date, pattern in patterns.items():
print(f"{date}: {pattern['availability_percentage']:.1f}% available")The FreeBusy functionality in GCSA provides powerful tools for availability checking, conflict detection, and scheduling optimization, making it essential for building calendar coordination and meeting scheduling applications.
Install with Tessl CLI
npx tessl i tessl/pypi-gcsa