├── app ├── __init__.py ├── mongo │ ├── __init__.py │ └── db.py ├── utils │ ├── __init__.py │ └── decorators.py ├── google_utility │ ├── __init__.py │ ├── calendar.py │ └── auth.py ├── .flaskenv ├── templates │ ├── index.html │ └── events.html ├── config.py └── main.py ├── .dockerignore ├── .gitignore ├── usage_screenshots ├── index.png ├── oauth.png ├── docker.png ├── postman.png ├── api_limit.png ├── events_get.png └── events_post.png ├── requirements.txt ├── Dockerfile └── README.md /app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/mongo/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/google_utility/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/.flaskenv: -------------------------------------------------------------------------------- 1 | FLASK_DEBUG = True 2 | FLASK_APP=main -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | app/google/client_secret.json 2 | app/.env -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | v 2 | .env 3 | ask.py 4 | __pycache__/ 5 | client_secret.json 6 | -------------------------------------------------------------------------------- /usage_screenshots/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/index.png -------------------------------------------------------------------------------- /usage_screenshots/oauth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/oauth.png -------------------------------------------------------------------------------- /usage_screenshots/docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/docker.png -------------------------------------------------------------------------------- /usage_screenshots/postman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/postman.png -------------------------------------------------------------------------------- /usage_screenshots/api_limit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/api_limit.png -------------------------------------------------------------------------------- /usage_screenshots/events_get.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/events_get.png -------------------------------------------------------------------------------- /usage_screenshots/events_post.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/flask-calendar-event-api/main/usage_screenshots/events_post.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | google-auth-oauthlib 3 | google-auth-httplib2 4 | google-api-python-client 5 | pymongo 6 | Flask-Limiter 7 | pytz 8 | python-dotenv 9 | google-auth-oauthlib 10 | -------------------------------------------------------------------------------- /app/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello 5 | 6 | 7 |

Hello World

8 |

9 | 10 |

11 | 12 | 13 | -------------------------------------------------------------------------------- /app/templates/events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | Start date:
5 | End date:
6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.10-slim 2 | 3 | WORKDIR /code 4 | 5 | ENV LISTEN_PORT=5000 6 | EXPOSE 5000 7 | 8 | COPY requirements.txt . 9 | RUN python -m pip install --upgrade pip && pip install -r requirements.txt 10 | 11 | COPY app /code/app 12 | 13 | ENV MONGO_URI= 14 | ENV MONGO_DB= 15 | ENV MONGO_COLLECTION= 16 | ENV GOOGLE_CLIENT_ID= 17 | ENV CLIENT_SECRET= 18 | 19 | CMD cd app && python3 -m flask run --host=0.0.0.0 20 | -------------------------------------------------------------------------------- /app/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secrets 3 | 4 | class Config: 5 | SECRET_KEY = secrets.token_urlsafe(16) 6 | GOOGLE_CLIENT_SECRET = os.environ.get('GOOGLE_CLIENT_SECRET') 7 | GOOGLE_CLIENT_ID = os.environ.get('GOOGLE_CLIENT_ID') 8 | GOOGLE_CLIENT_JSON = os.environ.get('GOOGLE_CLIENT_JSON') 9 | GOOGLE_AUTH_SCOPE=[ 10 | "https://www.googleapis.com/auth/userinfo.profile", 11 | "https://www.googleapis.com/auth/userinfo.email", 12 | "openid", 13 | "https://www.googleapis.com/auth/calendar" 14 | ] 15 | GOOGLE_AUTH_REDIRECT_URI="http://localhost:5000/callback" 16 | -------------------------------------------------------------------------------- /app/google_utility/calendar.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, time 2 | import pytz 3 | from googleapiclient.discovery import build 4 | 5 | 6 | def get_calendar_events(start_date, end_date, credentials): 7 | service = build("calendar", "v3", credentials=credentials) 8 | start = datetime.combine(start_date, time.min).isoformat() + "Z" # 'Z' indicates UTC time 9 | end = datetime.combine(end_date, time.max).isoformat() + "Z" # 'Z' indicates UTC time 10 | 11 | events_result = ( 12 | service.events() 13 | .list( 14 | calendarId="primary", 15 | timeMin=start, 16 | timeMax=end, 17 | maxResults=10, 18 | singleEvents=True, 19 | orderBy="startTime", 20 | ) 21 | .execute() 22 | ) 23 | 24 | events = events_result.get("items", []) 25 | event_list = [] 26 | 27 | if not events: 28 | event_list.append("No upcoming events found.") 29 | else: 30 | for event in events: 31 | start = event["start"].get("dateTime", event["start"].get("date")) 32 | event_time = ( 33 | datetime.fromisoformat(start) 34 | .astimezone(pytz.timezone("Asia/Kolkata")) 35 | .strftime("%Y-%m-%d %H:%M:%S") 36 | ) 37 | event_list.append(f"{event_time} - {event['summary']}") 38 | 39 | return event_list 40 | -------------------------------------------------------------------------------- /app/mongo/db.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import google.oauth2.credentials 4 | from pymongo import MongoClient 5 | 6 | # Set up MongoDB client and collection 7 | mongo_client = MongoClient(os.environ.get('MONGO_URI')) 8 | mongo_db = mongo_client[os.environ.get('MONGO_DB')] 9 | mongo_collection = mongo_db[os.environ.get('MONGO_COLLECTION')] 10 | 11 | 12 | # Define MongoDB schema 13 | user_schema = { 14 | 'user_id': str, 15 | 'credentials_data': dict, 16 | 'credentials': str 17 | } 18 | 19 | class MongoDBError(Exception): 20 | pass 21 | 22 | 23 | def db_add_user(user_id, credentials): 24 | credentials_data = { 25 | 'token': credentials.token, 26 | 'refresh_token': credentials.refresh_token, 27 | 'token_uri': credentials.token_uri, 28 | 'client_id': credentials.client_id, 29 | 'client_secret': credentials.client_secret, 30 | 'scopes': credentials.scopes, 31 | 'expiry': credentials.expiry.isoformat(), 32 | } 33 | 34 | try: 35 | mongo_collection.update_one( 36 | {'user_id': user_id}, 37 | {'$set': { 38 | 'credentials_data': credentials_data, 39 | 'credentials': credentials.to_json() 40 | }}, 41 | upsert=True 42 | ) 43 | except Exception as e: 44 | raise MongoDBError(f"Error adding user to MongoDB: {e}") 45 | 46 | 47 | def db_get_user_credentials(user_id): 48 | try: 49 | user_doc = mongo_collection.find_one({'user_id': user_id}) 50 | if user_doc and 'credentials' in user_doc: 51 | credentials_info = json.loads(user_doc['credentials']) 52 | credentials = google.oauth2.credentials.Credentials.from_authorized_user_info(credentials_info) 53 | return credentials 54 | 55 | else: 56 | raise MongoDBError(f"User not found in the database") 57 | except Exception as e: 58 | raise MongoDBError(f"Error getting user credentials from MongoDB: {e}") 59 | -------------------------------------------------------------------------------- /app/utils/decorators.py: -------------------------------------------------------------------------------- 1 | from google.auth.exceptions import RefreshError 2 | from datetime import datetime 3 | from flask import ( 4 | redirect, 5 | request, 6 | session, 7 | url_for, 8 | ) 9 | from app.mongo.db import ( 10 | db_get_user_credentials, 11 | db_add_user, 12 | ) 13 | 14 | from app.google_utility.auth import refresh_token 15 | 16 | def user_id_is_required(function): 17 | def wrapper(*args, **kwargs): 18 | user_id = request.form.get('user_id') 19 | if user_id == None or user_id == "": 20 | if "user_id" in session: 21 | user_id = session["user_id"] 22 | else: 23 | return redirect(url_for("login")) 24 | 25 | return function(user_id=user_id,*args, **kwargs) 26 | return wrapper 27 | 28 | def validate_dates(function): 29 | def wrapper(*args, **kwargs): 30 | try: 31 | start_date_str = request.form.get("start_date") 32 | end_date_str = request.form.get("end_date") 33 | except: 34 | raise Exception("Enter both dates") 35 | 36 | try: 37 | start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date() 38 | end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date() 39 | 40 | if start_date > end_date: 41 | raise ValueError("Start date should be before end date.") 42 | dates = (start_date, end_date) 43 | except ValueError as ve: 44 | return f"ValueError: {str(ve)}" 45 | except TypeError as te: 46 | return f"TypeError: {str(te)}" 47 | except Exception as e: 48 | return f"Exception occurred: {str(e)}" 49 | 50 | return function(dates=dates, *args, **kwargs) 51 | 52 | return wrapper 53 | 54 | 55 | def fetchCredentials(function): 56 | def wrapper(*args, **kwargs): 57 | user_id = kwargs["user_id"] 58 | try: 59 | credentials = db_get_user_credentials(user_id) 60 | 61 | if not credentials.valid: 62 | try: 63 | credentials = refresh_token(credentials) 64 | db_add_user(user_id, credentials) 65 | except RefreshError: 66 | print(f"Error occured: {error}") 67 | return redirect(url_for("login")) 68 | 69 | except Exception as error: 70 | print(f"Error occured: {error}") 71 | return redirect(url_for("login")) 72 | return function(credentials=credentials, *args, **kwargs) 73 | return wrapper -------------------------------------------------------------------------------- /app/google_utility/auth.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from google.oauth2.credentials import Credentials 4 | from google_auth_oauthlib.flow import Flow 5 | import google.auth.exceptions 6 | import requests 7 | from google.oauth2 import id_token 8 | from app.config import Config 9 | from datetime import datetime, timedelta 10 | 11 | 12 | os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1" 13 | GOOGLE_CLIENT_ID = Config.GOOGLE_CLIENT_ID 14 | GOOGLE_CLIENT_SECRET = Config.GOOGLE_CLIENT_SECRET 15 | GOOGLE_CLIENT_JSON = Config.GOOGLE_CLIENT_JSON 16 | CLIENT_CONFIG = json.loads(GOOGLE_CLIENT_JSON) 17 | 18 | SCOPES=["https://www.googleapis.com/auth/userinfo.profile", 19 | "https://www.googleapis.com/auth/userinfo.email", 20 | "openid", 21 | "https://www.googleapis.com/auth/calendar"] 22 | 23 | 24 | flow = Flow.from_client_config( 25 | client_config=CLIENT_CONFIG, 26 | scopes=SCOPES) 27 | 28 | flow.redirect_uri = 'http://localhost:5000/callback' 29 | 30 | 31 | def get_id_info(credentials): 32 | token_request = google.auth.transport.requests.Request(session=requests.session()) 33 | try: 34 | id_info = id_token.verify_oauth2_token( 35 | id_token=credentials._id_token, 36 | request=token_request, 37 | audience=GOOGLE_CLIENT_ID 38 | ) 39 | return id_info 40 | except Exception as error: 41 | raise Exception(f"Error occured: {error}") 42 | 43 | 44 | 45 | def refresh_token(credentials): 46 | print(credentials.refresh_token) 47 | print(type(credentials.refresh_token)) 48 | 49 | params = { 50 | "client_id": credentials.client_id, 51 | "client_secret": credentials.client_secret, 52 | "refresh_token": credentials.refresh_token, 53 | "grant_type": "refresh_token" 54 | } 55 | 56 | try: 57 | response = requests.post("https://oauth2.googleapis.com/token", data=params) 58 | response.raise_for_status() 59 | # Calculate the new expiry date based on the current time and the expires_in value in the response 60 | new_expiry = datetime.now() + timedelta(seconds=response.json()['expires_in']) 61 | 62 | # Create the new Credentials object with the updated access token and expiry date 63 | new_credentials = Credentials( 64 | token=response.json()['access_token'], 65 | refresh_token=credentials.refresh_token, 66 | token_uri=credentials.token_uri, 67 | client_id=GOOGLE_CLIENT_ID, 68 | client_secret=GOOGLE_CLIENT_SECRET, 69 | scopes=SCOPES, 70 | expiry=new_expiry 71 | ) 72 | return new_credentials 73 | except requests.exceptions.HTTPError as error: 74 | # Handle HTTP errors 75 | raise Exception(f"HTTP error occurred: {error}") 76 | 77 | except Exception as error: 78 | raise Exception(f"Error occured: {error}") 79 | 80 | 81 | def get_flow(): 82 | return flow -------------------------------------------------------------------------------- /app/main.py: -------------------------------------------------------------------------------- 1 | from flask_limiter import Limiter 2 | from flask_limiter.util import get_remote_address 3 | from flask import ( 4 | Flask, 5 | abort, 6 | redirect, 7 | request, 8 | session, 9 | render_template, 10 | ) 11 | from app.utils.decorators import ( 12 | validate_dates, 13 | user_id_is_required, 14 | fetchCredentials, 15 | ) 16 | from app.google_utility.auth import ( 17 | get_id_info, 18 | get_flow, 19 | ) 20 | 21 | from app.mongo.db import ( 22 | db_add_user, 23 | ) 24 | 25 | from app.google_utility.calendar import get_calendar_events 26 | from app.config import Config 27 | 28 | app = Flask(__name__) 29 | app.config.from_object(Config) 30 | 31 | limiter = Limiter( 32 | get_remote_address, 33 | app=app, 34 | default_limits=["200 per day", "50 per hour"], 35 | storage_uri="memory://", 36 | ) 37 | 38 | @app.route("/") 39 | def index(): 40 | return render_template('index.html') 41 | 42 | 43 | @app.route("/login") 44 | def login(): 45 | try: 46 | authorization_url, state = get_flow().authorization_url() 47 | session["state"] = state 48 | return redirect(authorization_url) 49 | except Exception as error: 50 | print(f"Error occured: {error}") 51 | return redirect("/") 52 | 53 | 54 | @app.route("/logout") 55 | def logout(): 56 | session.clear() 57 | return redirect("/") 58 | 59 | 60 | @app.route("/callback") 61 | def callback(): 62 | try: 63 | get_flow().fetch_token(authorization_response=request.url) 64 | 65 | if not session["state"] == request.args["state"]: 66 | abort(500) # State does not match! 67 | 68 | credentials = get_flow().credentials 69 | id_info = get_id_info(credentials) 70 | 71 | session["user_id"] = id_info.get("sub") 72 | session["name"] = id_info.get("name") 73 | 74 | user_id = id_info.get("sub") 75 | db_add_user(user_id, credentials) 76 | return redirect("/events") 77 | except Exception as error: 78 | print(f"Error occured: {error}") 79 | return redirect("/") 80 | 81 | 82 | @app.route("/events", methods=["GET"]) 83 | @limiter.limit("5 per minute") 84 | def get_events(): 85 | return render_template("events.html") 86 | 87 | 88 | @app.route("/events", methods=["POST"]) 89 | @limiter.limit("5 per minute") 90 | @user_id_is_required 91 | @validate_dates 92 | @fetchCredentials 93 | def post_events(user_id, dates, credentials): 94 | start_date, end_date = dates 95 | 96 | try: 97 | event_list = get_calendar_events(start_date, end_date, credentials) 98 | return ( 99 | f"Your upcoming events are between " 100 | f"{start_date} and {end_date}:
{', '.join(event_list)}
" 101 | f"" 102 | ) 103 | except Exception as error: 104 | print(f"Error occured: {error}") 105 | return redirect("/") 106 | 107 | 108 | 109 | 110 | if __name__ == '__main__': 111 | app.run() 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Calendar API Integration with Python Flask 2 | This project demonstrates how to build a Python Flask API that connects to Google Calendar API. Users can connect their Google calendar with this service, store user tokens in the database, and retrieve calendar events for a specific date range using those tokens. 3 | 4 | # Installation 5 | ## MongoDB Setup 6 | Before running the application, you need to have a MongoDB instance running. You can create a free instance on MongoDB Atlas. 7 | 8 | ### Schema 9 | ``` 10 | # Define MongoDB schema 11 | user_schema = { 12 | 'user_id': str, 13 | 'credentials_data': dict, 14 | 'credentials': str 15 | } 16 | ``` 17 | 18 | ## Google Cloud Console Setup 19 | 1. Create a project on the Google Cloud Console. 20 | 2. Create OAuth2 credentials for the project by following the steps and Enable the Google Calendar API for the project. 21 | ### First setup a OAuth Screen 22 | - Keep user type external 23 | - You can keep the app domain, and the Authorized domain sections empty 24 | - Click on ADD or remove scopes: and select: 25 | ``` 26 | https://www.googleapis.com/auth/calendar 27 | ``` 28 | - In Test Users add your email id 29 | ### Setup credentials 30 | - create new credentials 31 | - Select OAuth client ID 32 | - In Applicatio type select web application 33 | - Under Authorized JavaScript origins add: 34 | ``` 35 | http://localhost:5000 36 | ``` 37 | - Under Authorized redirect URIs add: 38 | ``` 39 | http://localhost:5000/callback 40 | ``` 41 | 3. Download the client_secret.json file 42 | 43 | ## Setup Using Docker 44 | 1. Clone this repository using git clone https://github.com//google-calendar-api.git 45 | 2. Navigate to the cloned repository 46 | 3. Build a docker image using 47 | ``` 48 | docker build -t meet:1.0.0 . 49 | ``` 50 | 4. Run the docker image using the following command: 51 | ``` 52 | docker run -p 5000:5000 \ 53 | -e MONGO_URI= \ 54 | -e MONGO_DB= \ 55 | -e MONGO_COLLECTION= \ 56 | -e GOOGLE_CLIENT_ID= \ 57 | -e CLIENT_SECRET= \ 58 | meet:1.0.0 59 | ``` 60 | ### Note: Here MongoDB uri should be url encoded, and if there's a & symbol, it should come in "", also the CLIENT_SECRET recieves the json data of your client_secret.jsom file, wrapped in single inverted commas ''. 61 | 5. Access the application by visiting http://localhost:5000 on your web browser. 62 | 63 | ## Setup on Local Device 64 | 1. Clone this repository using git clone https://github.com//google-calendar-api.git 65 | 2. Navigate to the cloned repository using cd google-calendar-api 66 | 3. Create a Python virtual environment using python -m venv env 67 | 4. Activate the environment using source env/bin/activate (Linux/Mac) or env\Scripts\activate (Windows) 68 | 5. Install the dependencies using pip install -r requirements.txt 69 | 6. Create a .env file in the app directory with the following environment variables: 70 | ``` 71 | GOOGLE_CLIENT_SECRET= 72 | GOOGLE_CLIENT_ID= 73 | MONGO_URI= 74 | MONGO_DB= 75 | MONGO_COLLECTION= 76 | CLIENT_SECRET= 77 | ``` 78 | ### Note: Here MongoDB uri should be url encoded, and if there's a & symbol, it should come in "", also the CLIENT_SECRET recieves the json data of your client_secret.jsom file, wrapped in single inverted commas ''. 79 | 80 | 7. Start the application using 81 | ``` 82 | flask run 83 | ``` 84 | 8. Access the application by visiting http://localhost:5000 on your web browser. 85 | 86 | ## Usage 87 | ### Routes 88 | 89 | /: Home page 90 | /login: Login using Google OAuth2 91 | /logout: Clears the session 92 | /events (GET): Page to select the dates 93 | /events (POST): Pass in start_date, end_date, and user_id (Your Google ID) 94 | 95 | ## Features 96 | - Uses Flask-Limiter to limit the number of requests. The /events route is limited to 5 requests per minute for testing purposes. 97 | - Uses refresh_token to automatically refresh the tokens. If the refresh_token is expired, the user is redirected to the sign-in page. 98 | 99 | ## Usage screenshots 100 | 1. Index page 101 | 102 | ![Index Page](usage_screenshots/index.png) 103 | 104 | 2. OAuth page 105 | 106 | ![OAuth page](usage_screenshots/oauth.png) 107 | 108 | 3. Events range page 109 | 110 | ![Events range page](usage_screenshots/events_get.png) 111 | 112 | 4. Events result page | Post request 113 | 114 | ![Events result page](usage_screenshots/events_post.png) 115 | 116 | 5. Limit handling 117 | 118 | ![limit handling](usage_screenshots/api_limit.png) 119 | 120 | 6. Postman post request 121 | 122 | ![Postman post request](usage_screenshots/postman.png) 123 | 124 | 7. Docker hosted 125 | 126 | ![Docker Hosted](usage_screenshots/docker.png) 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | --------------------------------------------------------------------------------