import bisect from datetime import datetime, timedelta from flask import Flask, jsonify, make_response, redirect, request from google.auth.transport.requests import Request from google.oauth2.credentials import Credentials from googleapiclient.discovery import build import pytz # Monday to Friday WEEK_DAYS = {1, 2, 3, 4, 5} # 9 to 5 FROM = 9 TO = 17 TIMEZONE_NAME = 'America/New_York' TIMEZONE = pytz.timezone(TIMEZONE_NAME) AVAILABILITY_LIFETIME = timedelta(minutes=2) SCOPES = ['https://www.googleapis.com/auth/calendar'] UTC = pytz.timezone('UTC') creds = Credentials.from_authorized_user_file('creds.json', SCOPES) def read_datetime(dct): dt = datetime.fromisoformat(dct['dateTime']).replace(tzinfo=None) dt = pytz.timezone(dct['timeZone']).localize(dt) return dt def asiso(dt): return dt.isoformat()[:19] def asutciso(dt): return asiso(dt.astimezone(UTC)) + 'Z' def aslocal(dt): return dt.astimezone(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S') def get_availability(): if not creds.valid: creds.refresh(Request()) service = build('calendar', 'v3', credentials=creds) # Get events for the next 2 weeks now = datetime.utcnow() start = datetime(now.year, now.month, now.day) end = start + timedelta(days=14) events_result = service.events().list( calendarId='primary', timeMin=asutciso(now), timeMax=asutciso(end), maxResults=200, singleEvents=True, orderBy='startTime', ).execute() # Build a list of possible time slots for the next 2 weeks availability = [] for day in range(14): day = start + timedelta(days=day) if day.weekday() not in WEEK_DAYS: continue for hour in range(FROM, TO): for minute in (0, 30): availability.append(TIMEZONE.localize( day.replace(hour=hour, minute=minute), )) for event in events_result.get('items', []): event_start = read_datetime(event['start']) event_end = read_datetime(event['end']) # Find the first slot that starts less than 30min before this event first_idx = bisect.bisect_right( availability, event_start - timedelta(minutes=30), ) # Find the first slot that starts after this event ends last_idx = bisect.bisect_left( availability, event_end, ) # Remove the slots from the availability del availability[first_idx:last_idx] return availability availability = get_availability() availability_as_of_date = datetime.utcnow() app = Flask('calendar_booking_api') @app.route('/availability') def api_availability(): global availability, availability_as_of_date if ( availability_as_of_date is None or availability_as_of_date + AVAILABILITY_LIFETIME < datetime.utcnow() ): availability = get_availability() availability_as_of_date = datetime.utcnow() return jsonify({'availability': [asutciso(time) for time in availability]}) @app.route('/book', methods=['POST', 'OPTIONS']) def api_book(): # Allow cross-origin if request.method == 'OPTIONS': response = make_response() response.headers.add( 'Access-Control-Allow-Origin', 'https://vicky.rampin.org', ) response.headers.add( 'Access-Control-Allow-Headers', 'Content-Type', ) response.headers.add( 'Access-Control-Allow-Methods', 'POST', ) return response if not creds.valid: creds.refresh(Request()) service = build('calendar', 'v3', credentials=creds) full_name = request.form['name'] email = request.form['email'] topic = request.form['topic'] date = request.form['date'] end = start + timedelta(minutes=30) service.events().insert( calendarId='primary', body=dict( start={'dateTime': asiso(start), 'timeZone': TIMEZONE_NAME}, end={'dateTime': asiso(end), 'timeZone': TIMEZONE_NAME}, summary="Meeting with Vicky", description="Meeting scheduled from the web", ), ) response = redirect('https://vicky.rampin.org/book-successful', 303) response.headers.add( 'Access-Control-Allow-Origin', 'https://vicky.rampin.org', ) return response