calendar-booking/calendar_booking_api.py

117 lines
3.0 KiB
Python
Raw Permalink Normal View History

2022-05-12 22:28:34 +00:00
import bisect
from datetime import datetime, timedelta
from flask import Flask, jsonify, 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 = pytz.timezone('America/New_York')
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 asutciso(dt):
return dt.astimezone(UTC).isoformat()[:19] + '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'])
def api_book():
TODO