From 57d2dbadf692c8a3f81658e2b80d83e9f257e8b7 Mon Sep 17 00:00:00 2001 From: Robert Jambrecic Date: Tue, 18 Jun 2024 14:48:33 +0200 Subject: [PATCH] Implement first endpoint --- .gitignore | 2 + google_sheets/app.py | 83 ++++++++++++++++++++++++++++++++++++- google_sheets/db_helpers.py | 34 +++++++++++++++ pyproject.toml | 2 + schema.prisma | 20 +++++++++ 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 google_sheets/db_helpers.py create mode 100644 schema.prisma diff --git a/.gitignore b/.gitignore index 9e5161e..9a7d8c6 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ venv* htmlcov token .DS_Store + +client_secret.json diff --git a/google_sheets/app.py b/google_sheets/app.py index 0482918..e45c386 100644 --- a/google_sheets/app.py +++ b/google_sheets/app.py @@ -1,13 +1,18 @@ import datetime +import json import logging from os import environ -from typing import Annotated, List +from pathlib import Path +from typing import Annotated, Any, List, Union import python_weather -from fastapi import FastAPI, Query +from fastapi import FastAPI, HTTPException, Query +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build from pydantic import BaseModel from . import __version__ +from .db_helpers import get_db_connection, get_wasp_db_url __all__ = ["app"] @@ -26,6 +31,19 @@ title="google-sheets", ) +# Load client secret data from the JSON file +with Path("client_secret.json").open() as secret_file: + client_secret_data = json.load(secret_file) + +# OAuth2 configuration +oauth2_settings = { + "auth_uri": client_secret_data["web"]["auth_uri"], + "tokenUrl": client_secret_data["web"]["token_uri"], + "clientId": client_secret_data["web"]["client_id"], + "clientSecret": client_secret_data["web"]["client_secret"], + "redirectUri": client_secret_data["web"]["redirect_uris"][0], +} + class HourlyForecast(BaseModel): forecast_time: datetime.time @@ -79,3 +97,64 @@ async def get_weather( hourly_forecasts=hourly_forecasts, ) return weather_response + + +async def get_user(user_id: Union[int, str]) -> Any: + wasp_db_url = await get_wasp_db_url() + async with get_db_connection(db_url=wasp_db_url) as db: + user = await db.query_first( + f'SELECT * from "User" where id={user_id}' # nosec: [B608] + ) + if not user: + raise HTTPException(status_code=404, detail=f"user_id {user_id} not found") + return user + + +async def load_user_credentials(user_id: Union[int, str]) -> Any: + await get_user(user_id=user_id) + async with get_db_connection() as db: + data = await db.gauth.find_unique_or_raise(where={"user_id": user_id}) + + return data.creds + + +def _get_sheet(user_credentials: Any, spreadshit_id: str, range: str) -> Any: + sheets_credentials = { + "refresh_token": user_credentials["refresh_token"], + "client_id": oauth2_settings["clientId"], + "client_secret": oauth2_settings["clientSecret"], + } + + creds = Credentials.from_authorized_user_info( + info=sheets_credentials, scopes=["https://www.googleapis.com/auth/spreadsheets"] + ) + service = build("sheets", "v4", credentials=creds) + + # Call the Sheets API + sheet = service.spreadsheets() + result = sheet.values().get(spreadsheetId=spreadshit_id, range=range).execute() + values = result.get("values", []) + + return values + + +@app.get("/sheet", description="Get data from a Google Sheet") +async def get_sheet( + user_id: Annotated[ + int, Query(description="The user ID for which the data is requested") + ], + spreadshit_id: Annotated[ + str, Query(description="ID of the Google Sheet to fetch data from") + ], + range: Annotated[ + str, + Query(description="The range of cells to fetch data from. E.g. 'Sheet1!A1:B2'"), + ], +) -> Union[str, List[List[str]]]: + user_credentials = await load_user_credentials(user_id) + values = _get_sheet(user_credentials, spreadshit_id, range) + + if not values: + return "No data found." + + return values # type: ignore[no-any-return] diff --git a/google_sheets/db_helpers.py b/google_sheets/db_helpers.py new file mode 100644 index 0000000..928e4b7 --- /dev/null +++ b/google_sheets/db_helpers.py @@ -0,0 +1,34 @@ +from contextlib import asynccontextmanager +from os import environ +from typing import AsyncGenerator, Optional + +from prisma import Prisma # type: ignore[attr-defined] + + +@asynccontextmanager +async def get_db_connection( + db_url: Optional[str] = None, +) -> AsyncGenerator[Prisma, None]: + if not db_url: + db_url = environ.get("DATABASE_URL", None) + if not db_url: + raise ValueError( + "No database URL provided nor set as environment variable 'DATABASE_URL'" + ) # pragma: no cover + if "connect_timeout" not in db_url: + db_url += "?connect_timeout=60" + db = Prisma(datasource={"url": db_url}) + await db.connect() + try: + yield db + finally: + await db.disconnect() + + +async def get_wasp_db_url() -> str: + curr_db_url = environ.get("DATABASE_URL") + wasp_db_name = environ.get("WASP_DB_NAME", "waspdb") + wasp_db_url = curr_db_url.replace(curr_db_url.split("/")[-1], wasp_db_name) # type: ignore[union-attr] + if "connect_timeout" not in wasp_db_url: + wasp_db_url += "?connect_timeout=60" + return wasp_db_url diff --git a/pyproject.toml b/pyproject.toml index f9fa5e1..4f9943c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,8 @@ dependencies = [ "pydantic>=2.3,<3", "fastapi>=0.110.2", "python-weather==2.0.3", + "prisma==0.13.1", + "google-api-python-client==2.133.0", ] [project.optional-dependencies] diff --git a/schema.prisma b/schema.prisma new file mode 100644 index 0000000..24477d1 --- /dev/null +++ b/schema.prisma @@ -0,0 +1,20 @@ +datasource db { + // could be postgresql or mysql + provider = "postgresql" + url = env("DATABASE_URL") +} + +generator db { + provider = "prisma-client-py" + interface = "asyncio" + recursive_type_depth = 5 +} + +model GAuth { + id String @id @default(cuid()) + created_at DateTime @default(now()) + updated_at DateTime @updatedAt + user_id Int @unique + creds Json + info Json +}