Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate with captn #44

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 135 additions & 63 deletions google_sheets/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
import logging
from datetime import datetime
from os import environ
from typing import Annotated, Dict, List, Literal, Union
from typing import Annotated, Any, Dict, List, Optional, Union

import httpx
import pandas as pd
from fastapi import FastAPI, HTTPException, Query, Response, status
from fastapi import Body, FastAPI, HTTPException, Query, Response, status
from fastapi.responses import RedirectResponse
from googleapiclient.errors import HttpError

Expand Down Expand Up @@ -59,34 +59,48 @@ async def get_login_url(
user_id: Annotated[
int, Query(description="The user ID for which the data is requested")
],
conv_uuid: Annotated[
Optional[str], Query(description="The conversation UUID")
] = None,
force_new_login: Annotated[bool, Query(description="Force new login")] = False,
) -> Dict[str, str]:
_check_parameters_are_not_none({"conv_uuid": conv_uuid})
if not force_new_login:
is_authenticated = await is_authenticated_for_ads(user_id=user_id)
if is_authenticated:
return {"login_url": "User is already authenticated"}

google_oauth_url = get_google_oauth_url(user_id)
google_oauth_url = get_google_oauth_url(user_id, conv_uuid) # type: ignore
markdown_url = f"To navigate Google Ads waters, I require access to your account. Please [click here]({google_oauth_url}) to grant permission."
return {"login_url": markdown_url}


@app.get("/login/success", description="Get the success message after login")
async def get_login_success() -> Dict[str, str]:
return {"login_success": "You have successfully logged in"}
def _check_parameters_are_not_none(kwargs: Dict[str, Any]) -> None:
error_message = "The following parameters are required: "
missing_parameters = [key for key, value in kwargs.items() if value is None]
if missing_parameters:
error_message += ", ".join(missing_parameters)
raise HTTPException(status_code=400, detail=error_message)


REDIRECT_DOMAIN = environ.get("REDIRECT_DOMAIN", "http://localhost:3000")


# Route 2: Save user credentials/token to a JSON file
@app.get("/login/callback")
async def login_callback(
code: Annotated[
str, Query(description="The authorization code received after successful login")
str,
Query(description="The authorization code received after successful login"),
],
state: Annotated[str, Query(description="State")],
state: Annotated[Optional[str], Query(description="State")] = None,
) -> RedirectResponse:
if not state.isdigit():
_check_parameters_are_not_none({"state": state})
user_id_and_chat_uuid = state.split(":") # type: ignore
if not user_id_and_chat_uuid[0].isdigit(): # type: ignore
raise HTTPException(status_code=400, detail="User ID must be an integer")
user_id = int(state)
user_id = int(user_id_and_chat_uuid[0])
chat_uuid = user_id_and_chat_uuid[1]

token_request_data = get_token_request_data(code)

Expand Down Expand Up @@ -122,12 +136,9 @@ async def login_callback(
},
)

# redirect_domain = environ.get("REDIRECT_DOMAIN", "https://captn.ai")
# logged_in_message = "I have successfully logged in"
# redirect_uri = f"{redirect_domain}/chat/{chat_uuid}?msg={logged_in_message}"
# return RedirectResponse(redirect_uri)
# redirect to success page
return RedirectResponse(url=f"{base_url}/login/success")
logged_in_message = "I have successfully logged in"
redirect_uri = f"{REDIRECT_DOMAIN}/chat/{chat_uuid}?msg={logged_in_message}"
return RedirectResponse(redirect_uri)


@app.get("/get-sheet", description="Get data from a Google Sheet")
Expand All @@ -136,16 +147,19 @@ async def get_sheet(
int, Query(description="The user ID for which the data is requested")
],
spreadsheet_id: Annotated[
str, Query(description="ID of the Google Sheet to fetch data from")
],
Optional[str], Query(description="ID of the Google Sheet to fetch data from")
] = None,
title: Annotated[
str,
Optional[str],
Query(description="The title of the sheet to fetch data from"),
],
] = None,
) -> Union[str, GoogleSheetValues]:
_check_parameters_are_not_none({"spreadsheet_id": spreadsheet_id, "title": title})
service = await build_service(user_id=user_id, service_name="sheets", version="v4")
values = await get_sheet_f(
service=service, spreadsheet_id=spreadsheet_id, range=title
service=service,
spreadsheet_id=spreadsheet_id, # type: ignore
range=title, # type: ignore
)

if not values:
Expand All @@ -163,22 +177,28 @@ async def update_sheet(
int, Query(description="The user ID for which the data is requested")
],
spreadsheet_id: Annotated[
str, Query(description="ID of the Google Sheet to fetch data from")
],
Optional[str], Query(description="ID of the Google Sheet to fetch data from")
] = None,
title: Annotated[
str,
Optional[str],
Query(description="The title of the sheet to update"),
],
sheet_values: GoogleSheetValues,
] = None,
sheet_values: Annotated[
Optional[GoogleSheetValues],
Body(embed=True, description="Values to be written to the Google Sheet"),
] = None,
) -> Response:
_check_parameters_are_not_none(
{"spreadsheet_id": spreadsheet_id, "title": title, "sheet_values": sheet_values}
)
service = await build_service(user_id=user_id, service_name="sheets", version="v4")

try:
await update_sheet_f(
service=service,
spreadsheet_id=spreadsheet_id,
range=title,
sheet_values=sheet_values,
spreadsheet_id=spreadsheet_id, # type: ignore
range=title, # type: ignore
sheet_values=sheet_values, # type: ignore
)
except HttpError as e:
raise HTTPException(status_code=e.status_code, detail=e._get_reason()) from e
Expand All @@ -202,17 +222,20 @@ async def create_sheet(
int, Query(description="The user ID for which the data is requested")
],
spreadsheet_id: Annotated[
str, Query(description="ID of the Google Sheet to fetch data from")
],
Optional[str], Query(description="ID of the Google Sheet to fetch data from")
] = None,
title: Annotated[
str,
Optional[str],
Query(description="The title of the new sheet"),
],
] = None,
) -> Response:
_check_parameters_are_not_none({"spreadsheet_id": spreadsheet_id, "title": title})
service = await build_service(user_id=user_id, service_name="sheets", version="v4")
try:
await create_sheet_f(
service=service, spreadsheet_id=spreadsheet_id, title=title
service=service,
spreadsheet_id=spreadsheet_id, # type: ignore
title=title, # type: ignore
)
except HttpError as e:
if (
Expand Down Expand Up @@ -257,13 +280,15 @@ async def get_all_sheet_titles(
int, Query(description="The user ID for which the data is requested")
],
spreadsheet_id: Annotated[
str, Query(description="ID of the Google Sheet to fetch data from")
],
Optional[str], Query(description="ID of the Google Sheet to fetch data from")
] = None,
) -> List[str]:
_check_parameters_are_not_none({"spreadsheet_id": spreadsheet_id})
service = await build_service(user_id=user_id, service_name="sheets", version="v4")
try:
sheets = await get_all_sheet_titles_f(
service=service, spreadsheet_id=spreadsheet_id
service=service,
spreadsheet_id=spreadsheet_id, # type: ignore
)
except HttpError as e:
raise HTTPException(status_code=e.status_code, detail=e._get_reason()) from e
Expand Down Expand Up @@ -295,32 +320,64 @@ async def get_all_sheet_titles(
]


def _validate_target_resource(target_resource: Optional[str]) -> None:
if target_resource not in ["ad", "keyword"]:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="The target resource should be either 'ad' or 'keyword'.",
)


@app.post(
"/process-data",
description="Process data to generate new ads or keywords based on the template",
)
async def process_data(
template_sheet_values: GoogleSheetValues,
new_campaign_sheet_values: GoogleSheetValues,
template_sheet_values: Annotated[
Optional[GoogleSheetValues],
Body(
embed=True,
description="Template values to be used for generating new ads or keywords",
),
] = None,
new_campaign_sheet_values: Annotated[
Optional[GoogleSheetValues],
Body(
embed=True,
description="New campaign values to be used for generating new ads or keywords",
),
] = None,
target_resource: Annotated[
Literal["ad", "keyword"], Query(description="The target resource to be updated")
],
Optional[str],
Query(
description="The target resource to be updated. This can be 'ad' or 'keyword'"
),
] = None,
) -> GoogleSheetValues:
_check_parameters_are_not_none(
{
"template_sheet_values": template_sheet_values,
"new_campaign_sheet_values": new_campaign_sheet_values,
"target_resource": target_resource,
}
)
_validate_target_resource(target_resource)
if (
len(template_sheet_values.values) < 2
or len(new_campaign_sheet_values.values) < 2
len(template_sheet_values.values) < 2 # type: ignore
or len(new_campaign_sheet_values.values) < 2 # type: ignore
):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Both template and new campaign data should have at least two rows (header and data).",
)
try:
template_df = pd.DataFrame(
template_sheet_values.values[1:], columns=template_sheet_values.values[0]
template_sheet_values.values[1:], # type: ignore
columns=template_sheet_values.values[0], # type: ignore
)
new_campaign_df = pd.DataFrame(
new_campaign_sheet_values.values[1:],
columns=new_campaign_sheet_values.values[0],
new_campaign_sheet_values.values[1:], # type: ignore
columns=new_campaign_sheet_values.values[0], # type: ignore
)
except Exception as e:
raise HTTPException(
Expand Down Expand Up @@ -353,7 +410,10 @@ async def process_data(

processed_df = process_data_f(template_df, new_campaign_df)

validated_df = validate_output_data(processed_df, target_resource)
validated_df = validate_output_data(
processed_df,
target_resource, # type: ignore
)

values = [validated_df.columns.tolist(), *validated_df.values.tolist()]

Expand All @@ -369,23 +429,38 @@ async def process_spreadsheet(
int, Query(description="The user ID for which the data is requested")
],
template_spreadsheet_id: Annotated[
str, Query(description="ID of the Google Sheet with the template data")
],
Optional[str],
Query(description="ID of the Google Sheet with the template data"),
] = None,
template_sheet_title: Annotated[
str,
Optional[str],
Query(description="The title of the sheet with the template data"),
],
] = None,
new_campaign_spreadsheet_id: Annotated[
str, Query(description="ID of the Google Sheet with the new campaign data")
],
Optional[str],
Query(description="ID of the Google Sheet with the new campaign data"),
] = None,
new_campaign_sheet_title: Annotated[
str,
Optional[str],
Query(description="The title of the sheet with the new campaign data"),
],
] = None,
target_resource: Annotated[
Literal["ad", "keyword"], Query(description="The target resource to be updated")
],
) -> Response:
Optional[str],
Query(
description="The target resource to be updated, options: 'ad' or 'keyword'"
),
] = None,
) -> str:
_check_parameters_are_not_none(
{
"template_spreadsheet_id": template_spreadsheet_id,
"template_sheet_title": template_sheet_title,
"new_campaign_spreadsheet_id": new_campaign_spreadsheet_id,
"new_campaign_sheet_title": new_campaign_sheet_title,
"target_resource": target_resource,
}
)
_validate_target_resource(target_resource)
template_values = await get_sheet(
user_id=user_id,
spreadsheet_id=template_spreadsheet_id,
Expand Down Expand Up @@ -417,7 +492,7 @@ async def process_spreadsheet(
)

title = (
f"Captn - {target_resource.capitalize()}s {datetime.now():%Y-%m-%d %H:%M:%S}"
f"Captn - {target_resource.capitalize()}s {datetime.now():%Y-%m-%d %H:%M:%S}" # type: ignore
)
await create_sheet(
user_id=user_id,
Expand All @@ -431,7 +506,4 @@ async def process_spreadsheet(
sheet_values=processed_values,
)

return Response(
status_code=status.HTTP_201_CREATED,
content=f"Sheet with the name 'Captn - {target_resource.capitalize()}s' has been created successfully.",
)
return f"Sheet with the name 'Captn - {target_resource.capitalize()}s' has been created successfully." # type: ignore
5 changes: 3 additions & 2 deletions google_sheets/google_api/oauth_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@
}


def get_google_oauth_url(user_id: int) -> str:
def get_google_oauth_url(user_id: int, conv_uuid: str) -> str:
state = f"{user_id}:{conv_uuid}"
google_oauth_url = (
f"{oauth2_settings['auth_uri']}?client_id={oauth2_settings['clientId']}"
f"&redirect_uri={oauth2_settings['redirectUri']}&response_type=code"
f"&scope={urllib.parse.quote_plus('email https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.metadata.readonly')}"
f"&access_type=offline&prompt=consent&state={user_id}"
f"&access_type=offline&prompt=consent&state={state}"
)
return google_oauth_url

Expand Down
Loading
Loading