From f3d836836a3179797cb15e17541713ce45b4cd52 Mon Sep 17 00:00:00 2001 From: rjambrecic <32619626+rjambrecic@users.noreply.github.com> Date: Thu, 27 Jun 2024 06:33:28 +0200 Subject: [PATCH 1/2] Implement endpoints for creating and updating sheet (#30) * wip * Refactoring * Fix tests * Refactoring * Add tests for create-sheet endpoint * Initial update-sheet implementation * Asyncify part of update-sheet endpoint * wip * Update tests * Refactoring wip * Refactoring * Implement get-all-sheet-titles endpoint * Update tests --- google_sheets/app.py | 223 ++++++++------ google_sheets/google_api/__init__.py | 25 ++ google_sheets/google_api/oauth_settings.py | 40 +++ google_sheets/google_api/service.py | 122 ++++++++ google_sheets/model.py | 9 + tests/app/test_app.py | 331 ++++++++++++++++++++- 6 files changed, 641 insertions(+), 109 deletions(-) create mode 100644 google_sheets/google_api/__init__.py create mode 100644 google_sheets/google_api/oauth_settings.py create mode 100644 google_sheets/google_api/service.py create mode 100644 google_sheets/model.py diff --git a/google_sheets/app.py b/google_sheets/app.py index a0b21ad..2088496 100644 --- a/google_sheets/app.py +++ b/google_sheets/app.py @@ -1,20 +1,27 @@ import json import logging -import urllib.parse from os import environ -from pathlib import Path -from typing import Annotated, Any, Dict, List, Union +from typing import Annotated, Dict, List, Union import httpx -from asyncify import asyncify -from fastapi import FastAPI, HTTPException, Query, Request +from fastapi import FastAPI, HTTPException, Query, Request, Response, status from fastapi.responses import RedirectResponse -from google.oauth2.credentials import Credentials -from googleapiclient.discovery import build -from prisma.errors import RecordNotFoundError +from googleapiclient.errors import HttpError from . import __version__ from .db_helpers import get_db_connection +from .google_api import ( + build_service, + create_sheet_f, + get_all_sheet_titles_f, + get_files_f, + get_google_oauth_url, + get_sheet_f, + get_token_request_data, + oauth2_settings, + update_sheet_f, +) +from .model import GoogleSheetValues __all__ = ["app"] @@ -33,19 +40,6 @@ 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], -} - async def is_authenticated_for_ads(user_id: int) -> bool: async with get_db_connection() as db: @@ -68,12 +62,7 @@ async def get_login_url( if is_authenticated: return {"login_url": "User is already authenticated"} - 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}" - ) + google_oauth_url = get_google_oauth_url(user_id) 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} @@ -92,13 +81,7 @@ async def login_callback( raise HTTPException(status_code=400, detail="User ID must be an integer") user_id = int(state) - token_request_data = { - "code": code, - "client_id": oauth2_settings["clientId"], - "client_secret": oauth2_settings["clientSecret"], - "redirect_uri": oauth2_settings["redirectUri"], - "grant_type": "authorization_code", - } + token_request_data = get_token_request_data(code) async with httpx.AsyncClient() as client: response = await client.post( @@ -140,48 +123,7 @@ async def login_callback( return RedirectResponse(url=f"{base_url}/login/success") -async def load_user_credentials(user_id: Union[int, str]) -> Any: - async with get_db_connection() as db: - try: - data = await db.gauth.find_unique_or_raise(where={"user_id": user_id}) # type: ignore[typeddict-item] - except RecordNotFoundError as e: - raise HTTPException( - status_code=404, detail="User hasn't grant access yet!" - ) from e - - return data.creds - - -async def _build_service(user_id: int, service_name: str, version: str) -> Any: - user_credentials = await load_user_credentials(user_id) - sheets_credentials: Dict[str, str] = { - "refresh_token": user_credentials["refresh_token"], - "client_id": oauth2_settings["clientId"], - "client_secret": oauth2_settings["clientSecret"], - } - - creds = Credentials.from_authorized_user_info( # type: ignore[no-untyped-call] - info=sheets_credentials, - scopes=[ - "https://www.googleapis.com/auth/spreadsheets", - "https://www.googleapis.com/auth/drive.metadata.readonly", - ], - ) - service = build(serviceName=service_name, version=version, credentials=creds) - return service - - -@asyncify # type: ignore[misc] -def _get_sheet(service: Any, spreadsheet_id: str, range: str) -> Any: - # Call the Sheets API - sheet = service.spreadsheets() - result = sheet.values().get(spreadsheetId=spreadsheet_id, range=range).execute() - values = result.get("values", []) - - return values - - -@app.get("/sheet", description="Get data from a Google Sheet") +@app.get("/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") @@ -189,14 +131,14 @@ async def get_sheet( spreadsheet_id: Annotated[ str, Query(description="ID of the Google Sheet to fetch data from") ], - range: Annotated[ + title: Annotated[ str, - Query(description="The range of cells to fetch data from. E.g. 'Sheet1!A1:B2'"), + Query(description="The title of the sheet to fetch data from"), ], ) -> Union[str, List[List[str]]]: - service = await _build_service(user_id=user_id, service_name="sheets", version="v4") - values = await _get_sheet( - service=service, spreadsheet_id=spreadsheet_id, range=range + 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 ) if not values: @@ -205,20 +147,85 @@ async def get_sheet( return values # type: ignore[no-any-return] -@asyncify # type: ignore[misc] -def _get_files(service: Any) -> List[Dict[str, str]]: - # Call the Drive v3 API - results = ( - service.files() - .list( - q="mimeType='application/vnd.google-apps.spreadsheet'", - pageSize=100, # The default value is 100 - fields="nextPageToken, files(id, name)", +@app.post( + "/update-sheet", + description="Update data in a Google Sheet within the existing spreadsheet", +) +async def update_sheet( + user_id: Annotated[ + 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") + ], + title: Annotated[ + str, + Query(description="The title of the sheet to update"), + ], + sheet_values: GoogleSheetValues, +) -> Response: + 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, ) - .execute() + except HttpError as e: + raise HTTPException(status_code=e.status_code, detail=e._get_reason()) from e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) + ) from e + + return Response( + status_code=status.HTTP_200_OK, + content=f"Sheet with the name '{title}' has been updated successfully.", + ) + + +@app.post( + "/create-sheet", + description="Create a new Google Sheet within the existing spreadsheet", +) +async def create_sheet( + user_id: Annotated[ + 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") + ], + title: Annotated[ + str, + Query(description="The title of the new sheet"), + ], +) -> Response: + 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 + ) + except HttpError as e: + if ( + e.status_code == status.HTTP_400_BAD_REQUEST + and f'A sheet with the name "{title}" already exists' in e._get_reason() + ): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f'A sheet with the name "{title}" already exists. Please enter another name.', + ) from e + raise HTTPException(status_code=e.status_code, detail=e._get_reason()) from e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) + ) from e + + return Response( + status_code=status.HTTP_201_CREATED, + content=f"Sheet with the name '{title}' has been created successfully.", ) - items = results.get("files", []) - return items # type: ignore[no-any-return] @app.get("/get-all-file-names", description="Get all sheets associated with the user") @@ -227,8 +234,34 @@ async def get_all_file_names( int, Query(description="The user ID for which the data is requested") ], ) -> Dict[str, str]: - service = await _build_service(user_id=user_id, service_name="drive", version="v3") - files: List[Dict[str, str]] = await _get_files(service=service) + service = await build_service(user_id=user_id, service_name="drive", version="v3") + files: List[Dict[str, str]] = await get_files_f(service=service) # create dict where key is id and value is name files_dict = {file["id"]: file["name"] for file in files} return files_dict + + +@app.get( + "/get-all-sheet-titles", + description="Get all sheet titles within a Google Spreadsheet", +) +async def get_all_sheet_titles( + user_id: Annotated[ + 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") + ], +) -> List[str]: + 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 + ) + except HttpError as e: + raise HTTPException(status_code=e.status_code, detail=e._get_reason()) from e + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(e) + ) from e + return sheets diff --git a/google_sheets/google_api/__init__.py b/google_sheets/google_api/__init__.py new file mode 100644 index 0000000..228ee50 --- /dev/null +++ b/google_sheets/google_api/__init__.py @@ -0,0 +1,25 @@ +from .oauth_settings import ( + get_google_oauth_url, + get_token_request_data, + oauth2_settings, +) +from .service import ( + build_service, + create_sheet_f, + get_all_sheet_titles_f, + get_files_f, + get_sheet_f, + update_sheet_f, +) + +__all__ = [ + "build_service", + "create_sheet_f", + "get_all_sheet_titles_f", + "get_files_f", + "get_google_oauth_url", + "get_sheet_f", + "get_token_request_data", + "oauth2_settings", + "update_sheet_f", +] diff --git a/google_sheets/google_api/oauth_settings.py b/google_sheets/google_api/oauth_settings.py new file mode 100644 index 0000000..fc55d69 --- /dev/null +++ b/google_sheets/google_api/oauth_settings.py @@ -0,0 +1,40 @@ +import json +import urllib.parse +from pathlib import Path +from typing import Any, Dict + +__all__ = ["get_google_oauth_url", "get_token_request_data", "oauth2_settings"] + + +# 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], +} + + +def get_google_oauth_url(user_id: int) -> str: + 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}" + ) + return google_oauth_url + + +def get_token_request_data(code: str) -> Dict[str, Any]: + return { + "code": code, + "client_id": oauth2_settings["clientId"], + "client_secret": oauth2_settings["clientSecret"], + "redirect_uri": oauth2_settings["redirectUri"], + "grant_type": "authorization_code", + } diff --git a/google_sheets/google_api/service.py b/google_sheets/google_api/service.py new file mode 100644 index 0000000..35ccc5e --- /dev/null +++ b/google_sheets/google_api/service.py @@ -0,0 +1,122 @@ +from typing import Any, Dict, List, Union + +from asyncify import asyncify +from fastapi import HTTPException +from google.oauth2.credentials import Credentials +from googleapiclient.discovery import build +from prisma.errors import RecordNotFoundError + +from ..db_helpers import get_db_connection +from ..model import GoogleSheetValues +from .oauth_settings import oauth2_settings + +__all__ = [ + "build_service", + "create_sheet_f", + "get_all_sheet_titles_f", + "get_files_f", + "get_sheet_f", + "update_sheet_f", +] + + +async def _load_user_credentials(user_id: Union[int, str]) -> Any: + async with get_db_connection() as db: + try: + data = await db.gauth.find_unique_or_raise(where={"user_id": user_id}) # type: ignore[typeddict-item] + except RecordNotFoundError as e: + raise HTTPException( + status_code=404, detail="User hasn't grant access yet!" + ) from e + + return data.creds + + +async def build_service(user_id: int, service_name: str, version: str) -> Any: + user_credentials = await _load_user_credentials(user_id) + sheets_credentials: Dict[str, str] = { + "refresh_token": user_credentials["refresh_token"], + "client_id": oauth2_settings["clientId"], + "client_secret": oauth2_settings["clientSecret"], + } + + creds = Credentials.from_authorized_user_info( # type: ignore[no-untyped-call] + info=sheets_credentials, + scopes=[ + "https://www.googleapis.com/auth/spreadsheets", + "https://www.googleapis.com/auth/drive.metadata.readonly", + ], + ) + service = build(serviceName=service_name, version=version, credentials=creds) + return service + + +@asyncify # type: ignore[misc] +def get_files_f(service: Any) -> List[Dict[str, str]]: + # Call the Drive v3 API + results = ( + service.files() + .list( + q="mimeType='application/vnd.google-apps.spreadsheet'", + pageSize=100, # The default value is 100 + fields="nextPageToken, files(id, name)", + ) + .execute() + ) + items = results.get("files", []) + return items # type: ignore[no-any-return] + + +@asyncify # type: ignore[misc] +def get_sheet_f(service: Any, spreadsheet_id: str, range: str) -> Any: + # Call the Sheets API + sheet = service.spreadsheets() + result = sheet.values().get(spreadsheetId=spreadsheet_id, range=range).execute() + values = result.get("values", []) + + return values + + +@asyncify # type: ignore[misc] +def update_sheet_f( + service: Any, spreadsheet_id: str, range: str, sheet_values: GoogleSheetValues +) -> None: + # Values are intended to be a 2d array. + # They should be in the form of [[ 'a', 'b', 'c'], [ 1, 2, 3 ]] + request = ( + service.spreadsheets() + .values() + .update( + spreadsheetId=spreadsheet_id, + valueInputOption="RAW", + range=range, + body={"majorDimension": "ROWS", "values": sheet_values.values}, + ) + ) + request.execute() + + +@asyncify # type: ignore[misc] +def create_sheet_f(service: Any, spreadsheet_id: str, title: str) -> None: + body = { + "requests": [ + { + "addSheet": { + "properties": { + "title": title, + } + } + } + ] + } + request = service.spreadsheets().batchUpdate( + spreadsheetId=spreadsheet_id, body=body + ) + request.execute() + + +@asyncify # type: ignore[misc] +def get_all_sheet_titles_f(service: Any, spreadsheet_id: str) -> List[str]: + sheet_metadata = service.spreadsheets().get(spreadsheetId=spreadsheet_id).execute() + sheets = sheet_metadata.get("sheets", "") + return [sheet["properties"]["title"] for sheet in sheets] diff --git a/google_sheets/model.py b/google_sheets/model.py new file mode 100644 index 0000000..e38fcd1 --- /dev/null +++ b/google_sheets/model.py @@ -0,0 +1,9 @@ +from typing import Any, List + +from pydantic import BaseModel, Field + + +class GoogleSheetValues(BaseModel): + values: List[List[Any]] = Field( + ..., title="Values", description="Values to be written to the Google Sheet." + ) diff --git a/tests/app/test_app.py b/tests/app/test_app.py index 1df2241..96d982c 100644 --- a/tests/app/test_app.py +++ b/tests/app/test_app.py @@ -1,6 +1,9 @@ -from unittest.mock import patch +from typing import Optional, Union +from unittest.mock import MagicMock, patch +import pytest from fastapi.testclient import TestClient +from googleapiclient.errors import HttpError from google_sheets import __version__ as version from google_sheets.app import app @@ -8,10 +11,10 @@ client = TestClient(app) -class TestRoutes: +class TestGetSheet: def test_get_sheet(self) -> None: with patch( - "google_sheets.app.load_user_credentials", + "google_sheets.google_api.service._load_user_credentials", return_value={"refresh_token": "abcdf"}, ) as mock_load_user_credentials: excepted = [ @@ -21,24 +24,128 @@ def test_get_sheet(self) -> None: ["Campaign A", "Ad group A", "Keyword C"], ] with patch( - "google_sheets.app._get_sheet", return_value=excepted + "google_sheets.app.get_sheet_f", return_value=excepted ) as mock_get_sheet: response = client.get( - "/sheet?user_id=123&spreadsheet_id=abc&range=Sheet1" + "/get-sheet?user_id=123&spreadsheet_id=abc&title=Sheet1" ) mock_load_user_credentials.assert_called_once() mock_get_sheet.assert_called_once() assert response.status_code == 200 assert response.json() == excepted + +def _create_http_error_mock(reason: str, status: int) -> HttpError: + resp = MagicMock() + resp.reason = reason + resp.status = status + + return HttpError(resp=resp, content=b"") + + +class TestCreateSheet: + @pytest.mark.parametrize( + ("side_effect", "expected_status_code"), + [ + (None, 201), + ( + _create_http_error_mock( + 'A sheet with the name "Sheet2" already exists', 400 + ), + 400, + ), + (_create_http_error_mock("Bad Request", 400), 400), + (Exception("Some error"), 500), + ], + ) + def test_create_sheet( + self, + side_effect: Optional[Union[HttpError, Exception]], + expected_status_code: int, + ) -> None: + with ( + patch( + "google_sheets.google_api.service._load_user_credentials", + return_value={"refresh_token": "abcdf"}, + ) as mock_load_user_credentials, + patch( + "google_sheets.app.create_sheet_f", side_effect=[side_effect] + ) as mock_create_sheet, + ): + response = client.post( + "/create-sheet?user_id=123&spreadsheet_id=abc&title=Sheet2" + ) + mock_load_user_credentials.assert_called_once() + mock_create_sheet.assert_called_once() + assert response.status_code == expected_status_code + + +class TestGetAllSheetTitles: + def test_get_all_sheet_titles(self) -> None: + with ( + patch( + "google_sheets.google_api.service._load_user_credentials", + return_value={"refresh_token": "abcdf"}, + ) as mock_load_user_credentials, + patch( + "google_sheets.app.get_all_sheet_titles_f", + return_value=["Sheet1", "Sheet2"], + ) as mock_get_all_sheet_titles, + ): + expected = ["Sheet1", "Sheet2"] + response = client.get( + "/get-all-sheet-titles?user_id=123&spreadsheet_id=abc" + ) + mock_load_user_credentials.assert_called_once() + mock_get_all_sheet_titles.assert_called_once() + assert response.status_code == 200 + assert response.json() == expected + + +class TestUpdateSheet: + @pytest.mark.parametrize( + ("side_effect", "expected_status_code"), + [ + (None, 200), + (_create_http_error_mock("Bad Request", 400), 400), + (Exception("Some error"), 500), + ], + ) + def test_update_sheet( + self, + side_effect: Optional[Union[HttpError, Exception]], + expected_status_code: int, + ) -> None: + with ( + patch( + "google_sheets.google_api.service._load_user_credentials", + return_value={"refresh_token": "abcdf"}, + ) as mock_load_user_credentials, + patch( + "google_sheets.app.update_sheet_f", side_effect=[side_effect] + ) as mock_update_sheet, + ): + json_data = { + "values": [["Campaign", "Ad Group"], ["Campaign A", "Ad group A"]] + } + response = client.post( + "/update-sheet?user_id=123&spreadsheet_id=abc&title=Sheet1", + json=json_data, + ) + mock_load_user_credentials.assert_called_once() + mock_update_sheet.assert_called_once() + assert response.status_code == expected_status_code + + +class TestGetAllFileNames: def test_get_all_file_names(self) -> None: with ( patch( - "google_sheets.app.load_user_credentials", + "google_sheets.google_api.service._load_user_credentials", return_value={"refresh_token": "abcdf"}, ) as mock_load_user_credentials, patch( - "google_sheets.app._get_files", + "google_sheets.app.get_files_f", return_value=[ {"id": "abc", "name": "file1"}, {"id": "def", "name": "file2"}, @@ -52,6 +159,8 @@ def test_get_all_file_names(self) -> None: assert response.status_code == 200 assert response.json() == expected + +class TestOpenAPIJSON: def test_openapi(self) -> None: expected = { "openapi": "3.1.0", @@ -170,11 +279,11 @@ def test_openapi(self) -> None: }, } }, - "/sheet": { + "/get-sheet": { "get": { "summary": "Get Sheet", "description": "Get data from a Google Sheet", - "operationId": "get_sheet_sheet_get", + "operationId": "get_sheet_get_sheet_get", "parameters": [ { "name": "user_id", @@ -199,15 +308,15 @@ def test_openapi(self) -> None: "description": "ID of the Google Sheet to fetch data from", }, { - "name": "range", + "name": "title", "in": "query", "required": True, "schema": { "type": "string", - "description": "The range of cells to fetch data from. E.g. 'Sheet1!A1:B2'", - "title": "Range", + "description": "The title of the sheet to fetch data from", + "title": "Title", }, - "description": "The range of cells to fetch data from. E.g. 'Sheet1!A1:B2'", + "description": "The title of the sheet to fetch data from", }, ], "responses": { @@ -226,7 +335,7 @@ def test_openapi(self) -> None: }, }, ], - "title": "Response Get Sheet Sheet Get", + "title": "Response Get Sheet Get Sheet Get", } } }, @@ -244,6 +353,132 @@ def test_openapi(self) -> None: }, } }, + "/update-sheet": { + "post": { + "summary": "Update Sheet", + "description": "Update data in a Google Sheet within the existing spreadsheet", + "operationId": "update_sheet_update_sheet_post", + "parameters": [ + { + "name": "user_id", + "in": "query", + "required": True, + "schema": { + "type": "integer", + "description": "The user ID for which the data is requested", + "title": "User Id", + }, + "description": "The user ID for which the data is requested", + }, + { + "name": "spreadsheet_id", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "ID of the Google Sheet to fetch data from", + "title": "Spreadsheet Id", + }, + "description": "ID of the Google Sheet to fetch data from", + }, + { + "name": "title", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "The title of the sheet to update", + "title": "Title", + }, + "description": "The title of the sheet to update", + }, + ], + "requestBody": { + "required": True, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GoogleSheetValues" + } + } + }, + }, + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, + "/create-sheet": { + "post": { + "summary": "Create Sheet", + "description": "Create a new Google Sheet within the existing spreadsheet", + "operationId": "create_sheet_create_sheet_post", + "parameters": [ + { + "name": "user_id", + "in": "query", + "required": True, + "schema": { + "type": "integer", + "description": "The user ID for which the data is requested", + "title": "User Id", + }, + "description": "The user ID for which the data is requested", + }, + { + "name": "spreadsheet_id", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "ID of the Google Sheet to fetch data from", + "title": "Spreadsheet Id", + }, + "description": "ID of the Google Sheet to fetch data from", + }, + { + "name": "title", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "The title of the new sheet", + "title": "Title", + }, + "description": "The title of the new sheet", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": {"application/json": {"schema": {}}}, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, "/get-all-file-names": { "get": { "summary": "Get All File Names", @@ -288,9 +523,77 @@ def test_openapi(self) -> None: }, } }, + "/get-all-sheet-titles": { + "get": { + "summary": "Get All Sheet Titles", + "description": "Get all sheet titles within a Google Spreadsheet", + "operationId": "get_all_sheet_titles_get_all_sheet_titles_get", + "parameters": [ + { + "name": "user_id", + "in": "query", + "required": True, + "schema": { + "type": "integer", + "description": "The user ID for which the data is requested", + "title": "User Id", + }, + "description": "The user ID for which the data is requested", + }, + { + "name": "spreadsheet_id", + "in": "query", + "required": True, + "schema": { + "type": "string", + "description": "ID of the Google Sheet to fetch data from", + "title": "Spreadsheet Id", + }, + "description": "ID of the Google Sheet to fetch data from", + }, + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": {"type": "string"}, + "title": "Response Get All Sheet Titles Get All Sheet Titles Get", + } + } + }, + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + }, + }, + }, + } + }, }, "components": { "schemas": { + "GoogleSheetValues": { + "properties": { + "values": { + "items": {"items": {}, "type": "array"}, + "type": "array", + "title": "Values", + "description": "Values to be written to the Google Sheet.", + } + }, + "type": "object", + "required": ["values"], + "title": "GoogleSheetValues", + }, "HTTPValidationError": { "properties": { "detail": { From 1a37abf1e6833cfce617a6b3ae9ec570407d2aa5 Mon Sep 17 00:00:00 2001 From: Kumaran Rajendhiran Date: Fri, 28 Jun 2024 14:28:22 +0530 Subject: [PATCH 2/2] Always restart docker (#31) --- google-sheets-docker-compose.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/google-sheets-docker-compose.yaml b/google-sheets-docker-compose.yaml index eeac56d..a2b8918 100644 --- a/google-sheets-docker-compose.yaml +++ b/google-sheets-docker-compose.yaml @@ -12,6 +12,7 @@ services: - REDIRECT_DOMAIN=${REDIRECT_DOMAIN} - DATABASE_URL=${DATABASE_URL} - CLIENT_SECRET=${CLIENT_SECRET} + restart: always networks: - google-sheets