Skip to content

Commit

Permalink
GCalendar connector improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneP committed Dec 4, 2023
1 parent 3e632fc commit e4e8a29
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 59 deletions.
5 changes: 4 additions & 1 deletion gcalendar/.env-template
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
GCALENDAR_CONNECTOR_API_KEY=
GCALENDAR_SERVICE_ACCOUNT_INFO=
GCALENDAR_SEARCH_LIMIT=
# Connector Authorization
GCALENDAR__CONNECTOR_API_KEY=
65 changes: 57 additions & 8 deletions gcalendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,68 @@

This connects Cohere to Google Calendar, allowing searching events.

## Credentials
## Authentication

To use this connector, you will need to enable the Google Calendar API in a Google Cloud project. There are several steps to follow, please refer to the following [guide.](https://developers.google.com/calendar/api/quickstart/python)
This connector supports two types of authentication: Service Account and OAuth.

You will need to:
### Service Account

1. Enable the API
2. Configure OAuth consent: use the Google account that will be used for the Calendar integration
3. Create credentials for your app, making sure to select the "Desktop app" option, then download the `credentials.json` file
For service account authentication this connector requires two environment variables:

Once this is done, save the `credentials.json` file into this directory.
#### `GCALENDAR_SERVICE_ACCOUNT_INFO`
#### `GCALENDAR_CALENDAR_ID`

This connector will also require a `token.json` file that is generated automatically once you authorize the app. After you deploy it, head to your hosted connector server's `/ui` page. For example, `https://myconnector.com/ui`, and trigger an initial `/search` request. This will bring up the Google OAuth page, where you can login to the same email as step 2 earlier.
The `GCALENDAR_SERVICE_ACCOUNT_INFO` variable should contain the JSON content of the service account credentials file.
To get the credentials file, follow these steps:

1. [Create a project in Google Cloud Console](https://cloud.google.com/resource-manager/docs/creating-managing-projects).
2. [Create a service account](https://cloud.google.com/iam/docs/creating-managing-service-accounts)
and [activate the Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com) in
the Google Cloud Console.
3. [Create a service account key](https://cloud.google.com/iam/docs/creating-managing-service-account-keys) and download
the credentials file as JSON. The credentials file should look like this:

```json
{
"type": "service_account",
"project_id": "{project id}",
"private_key_id": "{private_key_id}",
"private_key": "{private_key}",
"client_email": "{client_email}",
"client_id": "{client_id}",
"auth_uri": "{auth_uri}",
"token_uri": "{token_uri}",
"auth_provider_x509_cert_url": "{auth_provider_x509_cert_url}",
"client_x509_cert_url": "{client_x509_cert_url}",
"universe_domain": "{universe_domain}"
}
```

4. Convert the JSON credentails to a string through `json.dumps(credentials)` and save the result in
the `GCALENDAR_SERVICE_ACCOUNT_INFO` environment variable.
5. Make sure
to [share the calendar you want to search with the service account email address](https://support.google.com/a/answer/7337554?hl=en).
6. Assign the `GCALENDAR_CALENDAR_ID` environment variable the value of the calendar owner's email address.

#### `GCALENDAR_CONNECTOR_API_KEY`

The `GCALENDAR_CONNECTOR_API_KEY` should contain an API key for the connector. This value must be present in
the `Authorization` header for all requests to the connector.

### OAuth

When using OAuth for authentication, the connector does not require any additional environment variables. Instead, the
OAuth flow should occur outside of the Connector and Cohere's API will forward the user's access token to this connector
through the `Authorization` header.


## Optional Configuration

This connector also supports a few optional environment variables to configure the search:

1. `GCALENDAR_SEARCH_LIMIT` - Number of results to return. Default is 10.
2. `GCALENDAR_CALENDAR_ID` - ID of the calendar to search in. If not provided, the search will be performed in the primary calendar.
For the service account authentication method, this value should be the email address of the calendar owner.

## Development

Expand Down
11 changes: 0 additions & 11 deletions gcalendar/credentials.json-template

This file was deleted.

4 changes: 4 additions & 0 deletions gcalendar/provider/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ def search(body):
except UpstreamProviderError as error:
logger.error(f"Upstream search error: {error.message}")
abort(502, error.message)
except AssertionError as error:
logger.error(f"GCalendar connector config error: {error}")
abort(502, f"GCalendar connector config error: {error}")

return {"results": data}


Expand Down
90 changes: 52 additions & 38 deletions gcalendar/provider/client.py
Original file line number Diff line number Diff line change
@@ -1,51 +1,52 @@
import logging
import os
import datetime
import logging

from flask import current_app as app
from flask import request, current_app as app
from google.auth.transport.requests import Request
from google.oauth2 import service_account
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

from . import UpstreamProviderError


logger = logging.getLogger(__name__)

client = None
AUTHORIZATION_HEADER = "Authorization"
BEARER_PREFIX = "Bearer "
DEFAULT_SEARCH_LIMIT = 20


class GoogleCalendarClient:
DEFAULT_SEARCH_LIMIT = 20
SCOPES = [
"https://www.googleapis.com/auth/calendar.readonly",
]

def __init__(self):
creds = None
# Handle Authentication
if os.path.exists("token.json"):
logger.debug("Found token.json file")
creds = Credentials.from_authorized_user_file("token.json", self.SCOPES)

# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
logger.debug("No valid credentials found")
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
"credentials.json", self.SCOPES
)
creds = flow.run_local_server(port=0)

# Save the credentials for the next run
with open("token.json", "w") as token:
token.write(creds.to_json())

self.service = build("calendar", "v3", credentials=creds)
def __init__(self, service_account_info, access_token, calendar_id, search_limit):
self.search_limit = search_limit
self.calendar_id = calendar_id
self.service = build(
"calendar",
"v3",
credentials=self._request_credentials(service_account_info, access_token),
)

def _request_credentials(self, service_account_info=None, access_token=None):
if service_account_info is not None:
logger.debug("Using service account credentials")
credentials = service_account.Credentials.from_service_account_info(
service_account_info, scopes=self.SCOPES
)
if credentials.expired or not credentials.valid:
credentials.refresh(Request())

return credentials
elif access_token is not None:
logger.debug("Using oauth credentials")
return Credentials(access_token)
else:
raise UpstreamProviderError(
"No service account or oauth credentials provided"
)

def _request(self, request):
try:
Expand All @@ -55,10 +56,11 @@ def _request(self, request):

def search_events(self, query):
now = datetime.datetime.utcnow().isoformat() + "Z" # 'Z' indicates UTC time

request = self.service.events().list(
calendarId="primary",
calendarId=self.calendar_id,
timeMin=now,
maxResults=self.DEFAULT_SEARCH_LIMIT,
maxResults=self.search_limit,
singleEvents=True,
orderBy="startTime",
q=query,
Expand All @@ -70,8 +72,20 @@ def search_events(self, query):


def get_client():
global client
if client is None:
client = GoogleCalendarClient()

return client
service_account_info = app.config.get("SERVICE_ACCOUNT_INFO", None)
access_token = get_access_token()
calendar_id = app.config.get("CALENDAR_ID", "primary")
search_limit = app.config.get("SEARCH_LIMIT", DEFAULT_SEARCH_LIMIT)
if service_account_info is None and access_token is None:
raise AssertionError("No service account or oauth credentials provided")

return GoogleCalendarClient(
service_account_info, access_token, calendar_id, search_limit
)


def get_access_token():
authorization_header = request.headers.get(AUTHORIZATION_HEADER, "")
if authorization_header.startswith(BEARER_PREFIX):
return authorization_header.removeprefix(BEARER_PREFIX)
return None
2 changes: 1 addition & 1 deletion gcalendar/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = ["Scott Mountenay <scott@lightsonsoftware.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.10"
python = "^3.11"
connexion = {version = "2.14.2", extras = ["swagger-ui"]}
python-dotenv = "^1.0.0"
flask = "2.2.5"
Expand Down

0 comments on commit e4e8a29

Please sign in to comment.