CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-gcsa

Simple API for Google Calendar management

Pending
Overview
Eval results
Files

free-busy.mddocs/

Free/Busy and Availability

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.

Core Imports

from gcsa.free_busy import FreeBusy, TimeRange, FreeBusyQueryError
from datetime import datetime, timedelta

Capabilities

FreeBusy Class

Represents 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
        """

TimeRange

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: datetime

FreeBusyQueryError

Exception 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
        """

Free/Busy Queries via GoogleCalendar

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
    """

Usage Examples

Basic Free/Busy Query

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}")

Multiple Calendar Availability

# 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")

Group-Based Free/Busy Queries

# 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}")

Finding Available Time Slots

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}")

Team Availability Analysis

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']}")

Conflict Detection

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")

Error Handling

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}")

Advanced Time Analysis

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

docs

access-control.md

attendees.md

calendars.md

conferences.md

core-operations.md

events.md

free-busy.md

index.md

recurrence.md

reminders.md

tile.json