├── 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 |
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 | 
103 |
104 | 2. OAuth page
105 |
106 | 
107 |
108 | 3. Events range page
109 |
110 | 
111 |
112 | 4. Events result page | Post request
113 |
114 | 
115 |
116 | 5. Limit handling
117 |
118 | 
119 |
120 | 6. Postman post request
121 |
122 | 
123 |
124 | 7. Docker hosted
125 |
126 | 
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------