diff --git a/app/app.py b/app/app.py index d298f23fe..7c8a883b8 100644 --- a/app/app.py +++ b/app/app.py @@ -27,14 +27,16 @@ from app.core.groups.groups_type import GroupType from app.core.log import LogConfig from app.dependencies import ( + get_db, get_redis_client, get_websocket_connection_manager, init_and_get_db_engine, ) from app.modules.module_list import module_list -from app.types.exceptions import ContentHTTPException +from app.types.exceptions import ContentHTTPException, GoogleAPIInvalidCredentialsError from app.types.sqlalchemy import Base from app.utils import initialization +from app.utils.google_api.google_api import GoogleAPI from app.utils.redis import limiter if TYPE_CHECKING: @@ -298,6 +300,18 @@ def get_application(settings: Settings, drop_db: bool = False) -> FastAPI: # https://fastapi.tiangolo.com/advanced/events/ @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator: + # Init Google API credentials + async for db in app.dependency_overrides.get( + get_db, + get_db, + )(): + google_api = GoogleAPI() + try: + await google_api.get_credentials(db, settings) + except GoogleAPIInvalidCredentialsError: + # We expect this error to be raised if the credentials were never set before + pass + ws_manager = app.dependency_overrides.get( get_websocket_connection_manager, get_websocket_connection_manager, diff --git a/app/dependencies.py b/app/dependencies.py index 32c594795..b416716a6 100644 --- a/app/dependencies.py +++ b/app/dependencies.py @@ -192,16 +192,14 @@ def get_notification_tool( ) -def get_drive_file_manager( - settings: Settings = Depends(get_settings), -) -> DriveFileManager: +def get_drive_file_manager() -> DriveFileManager: """ Dependency that returns the drive file manager. """ global drive_file_manage if drive_file_manage is None: - drive_file_manage = DriveFileManager(settings=settings) + drive_file_manage = DriveFileManager() return drive_file_manage diff --git a/app/modules/raid/endpoints_raid.py b/app/modules/raid/endpoints_raid.py index 67d057941..81690a4c1 100644 --- a/app/modules/raid/endpoints_raid.py +++ b/app/modules/raid/endpoints_raid.py @@ -31,6 +31,7 @@ ) from app.types.content_type import ContentType from app.types.module import Module +from app.utils.google_api.google_api import DriveGoogleAPI from app.utils.tools import ( get_core_data, get_file_from_data, @@ -121,6 +122,7 @@ async def update_participant( user: models_core.CoreUser = Depends(is_user), db: AsyncSession = Depends(get_db), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Update a participant @@ -213,7 +215,12 @@ async def update_participant( await cruds_raid.update_participant(participant_id, participant, is_minor, db) team = await cruds_raid.get_team_by_participant_id(participant_id, db) - await post_update_actions(team, db, drive_file_manager) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) @module.router.post( @@ -226,6 +233,7 @@ async def create_team( user: models_core.CoreUser = Depends(is_user), db: AsyncSession = Depends(get_db), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Create a team @@ -248,7 +256,12 @@ async def create_team( await cruds_raid.create_team(db_team, db) # We need to get the team from the db to have access to relationships created_team = await cruds_raid.get_team_by_id(team_id=db_team.id, db=db) - await post_update_actions(created_team, db, drive_file_manager) + await post_update_actions( + created_team, + db, + drive_file_manager, + settings=settings, + ) return created_team @@ -320,6 +333,7 @@ async def update_team( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Update a team @@ -331,7 +345,12 @@ async def update_team( raise HTTPException(status_code=403, detail="You can only edit your own team.") await cruds_raid.update_team(team_id, team, db) updated_team = await cruds_raid.get_team_by_id(team_id, db) - await post_update_actions(updated_team, db, drive_file_manager) + await post_update_actions( + updated_team, + db, + drive_file_manager, + settings=settings, + ) @module.router.delete( @@ -342,7 +361,7 @@ async def delete_team( team_id: str, db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), - drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Delete a team @@ -354,11 +373,12 @@ async def delete_team( await cruds_raid.delete_team(team_id, db) # We will try to delete PDF associated with the team from the Google Drive if team.file_id: - drive_file_manager.delete_file(team.file_id) - if team.captain.security_file.file_id: - drive_file_manager.delete_file(team.captain.security_file.file_id) - if team.second and team.second.security_file.file_id: - drive_file_manager.delete_file(team.second.security_file.file_id) + async with DriveGoogleAPI(db, settings) as google_api: + google_api.delete_file(team.file_id) + if team.captain.security_file.file_id: + google_api.delete_file(team.captain.security_file.file_id) + if team.second and team.second.security_file.file_id: + google_api.delete_file(team.second.security_file.file_id) @module.router.delete( @@ -493,13 +513,19 @@ async def validate_document( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Validate a document """ await cruds_raid.update_document_validation(document_id, validation, db) team = await cruds_raid.get_team_by_participant_id(user.id, db) - await post_update_actions(team, db, drive_file_manager) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) @module.router.post( @@ -513,6 +539,7 @@ async def set_security_file( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Confirm security file @@ -552,8 +579,14 @@ async def set_security_file( team.number, db, drive_file_manager, + settings, + ) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, ) - await post_update_actions(team, db, drive_file_manager) return created_security_file @@ -566,13 +599,22 @@ async def confirm_payment( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Confirm payment manually """ await cruds_raid.confirm_payment(participant_id, db) - team = await cruds_raid.get_team_by_participant_id(participant_id, db) - await post_update_actions(team, db, drive_file_manager) + team = await cruds_raid.get_team_by_participant_id( + participant_id, + db, + ) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) @module.router.post( @@ -584,6 +626,7 @@ async def confirm_t_shirt_payment( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Confirm T shirt payment @@ -597,7 +640,12 @@ async def confirm_t_shirt_payment( raise HTTPException(status_code=400, detail="T shirt size not set.") await cruds_raid.confirm_t_shirt_payment(participant_id, db) team = await cruds_raid.get_team_by_participant_id(participant_id, db) - await post_update_actions(team, db, drive_file_manager) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) @module.router.post( @@ -609,6 +657,7 @@ async def validate_attestation_on_honour( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Validate attestation on honour @@ -617,7 +666,12 @@ async def validate_attestation_on_honour( raise HTTPException(status_code=403, detail="You are not the participant") await cruds_raid.validate_attestation_on_honour(participant_id, db) team = await cruds_raid.get_team_by_participant_id(participant_id, db) - await post_update_actions(team, db, drive_file_manager) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) @module.router.post( @@ -664,6 +718,7 @@ async def join_team( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Join a team @@ -682,7 +737,8 @@ async def join_team( raise HTTPException(status_code=403, detail="You are already in a team.") if user_team.file_id: - drive_file_manager.delete_file(user_team.file_id) + async with DriveGoogleAPI(db, settings) as google_api: + google_api.delete_file(user_team.file_id) await cruds_raid.delete_team(user_team.id, db) team = await cruds_raid.get_team_by_id(invite_token.team_id, db) @@ -700,7 +756,12 @@ async def join_team( ) await cruds_raid.update_team_second_id(team.id, user.id, db) - await post_update_actions(team, db, drive_file_manager) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) await cruds_raid.delete_invite_token(invite_token.id, db) @@ -715,6 +776,7 @@ async def kick_team_member( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Leave a team @@ -736,7 +798,12 @@ async def kick_team_member( elif team.second_id != participant_id: raise HTTPException(status_code=404, detail="Participant not found.") await cruds_raid.update_team_second_id(team_id, None, db) - await post_update_actions(team, db, drive_file_manager) + await post_update_actions( + team, + db, + drive_file_manager, + settings=settings, + ) return await cruds_raid.get_team_by_id(team_id, db) @@ -751,6 +818,7 @@ async def merge_teams( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Merge two teams @@ -785,8 +853,14 @@ async def merge_teams( await cruds_raid.update_team_second_id(team1_id, team2.captain_id, db) await cruds_raid.delete_team(team2_id, db) if team2.file_id: - drive_file_manager.delete_file(team2.file_id) - await post_update_actions(team1, db, drive_file_manager) + async with DriveGoogleAPI(db, settings) as google_api: + google_api.delete_file(team2.file_id) + await post_update_actions( + team1, + db, + drive_file_manager, + settings=settings, + ) return await cruds_raid.get_team_by_id(team1_id, db) @@ -814,6 +888,7 @@ async def update_raid_information( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Update raid information @@ -863,6 +938,7 @@ async def update_raid_information( team.number, db, drive_file_manager, + settings, ) @@ -875,6 +951,7 @@ async def update_drive_folders( db: AsyncSession = Depends(get_db), user: models_core.CoreUser = Depends(is_user_a_member_of(GroupType.raid_admin)), drive_file_manager: DriveFileManager = Depends(get_drive_file_manager), + settings: Settings = Depends(get_settings), ): """ Update drive folders @@ -886,7 +963,7 @@ async def update_drive_folders( security_folder_id=None, ) await set_core_data(schemas_folders, db) - await drive_file_manager.init_folders(db=db) + await drive_file_manager.init_folders(db=db, settings=settings) @module.router.get( diff --git a/app/modules/raid/utils/drive/drive_file_manager.py b/app/modules/raid/utils/drive/drive_file_manager.py index ce9055e27..5fc0fb65a 100644 --- a/app/modules/raid/utils/drive/drive_file_manager.py +++ b/app/modules/raid/utils/drive/drive_file_manager.py @@ -1,162 +1,101 @@ import logging -import google.oauth2.credentials -import googleapiclient.http -from googleapiclient.discovery import build from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import Settings from app.modules.raid import coredata_raid +from app.utils.google_api.google_api import DriveGoogleAPI from app.utils.tools import get_core_data, set_core_data hyperion_error_logger = logging.getLogger("hyperion.error") class DriveFileManager: - def __init__(self, settings: Settings): - oauth_credentials = google.oauth2.credentials.Credentials( - token="", - refresh_token=settings.RAID_DRIVE_REFRESH_TOKEN, - token_uri="https://oauth2.googleapis.com/token", # noqa: S106 - client_id=settings.RAID_DRIVE_CLIENT_ID, - client_secret=settings.RAID_DRIVE_CLIENT_SECRET, - scopes=["https://www.googleapis.com/auth/drive"], - ) - - self.drive_service = build( - "drive", - "v3", - credentials=oauth_credentials, - developerKey=settings.RAID_DRIVE_API_KEY, - ) - + def __init__(self): self.REGISTERING_FOLDER_NAME = "Équipes" self.SECURITY_FOLDER_NAME = "Fiches sécurité" self.drive_folders: coredata_raid.RaidDriveFolders | None = None - async def init_folders(self, db: AsyncSession): - if not self.drive_folders: + async def init_folders(self, db: AsyncSession, settings: Settings) -> None: + if self.drive_folders: + hyperion_error_logger.info( + "Raid Registering: drive folders already initialized", + ) + else: + hyperion_error_logger.info( + "Raid Registering: creating drive folders", + ) self.drive_folders = await get_core_data(coredata_raid.RaidDriveFolders, db) if not self.drive_folders.parent_folder_id: hyperion_error_logger.error("No parent folder id found in database") return - if not self.drive_folders.registering_folder_id: - self.drive_folders.registering_folder_id = self.create_folder( - self.REGISTERING_FOLDER_NAME, - self.drive_folders.parent_folder_id, - ) - if not self.drive_folders.security_folder_id: - self.drive_folders.security_folder_id = self.create_folder( - self.SECURITY_FOLDER_NAME, - self.drive_folders.parent_folder_id, - ) + async with DriveGoogleAPI(db, settings) as google_api: + if not self.drive_folders.registering_folder_id: + self.drive_folders.registering_folder_id = google_api.create_folder( + self.REGISTERING_FOLDER_NAME, + self.drive_folders.parent_folder_id, + ) + if not self.drive_folders.security_folder_id: + self.drive_folders.security_folder_id = google_api.create_folder( + self.SECURITY_FOLDER_NAME, + self.drive_folders.parent_folder_id, + ) await set_core_data( self.drive_folders, db, ) - def create_folder(self, folder_name: str, parent_folder_id: str) -> str: - file_metadata = { - "name": folder_name, - "mimeType": "application/vnd.google-apps.folder", - "parents": [parent_folder_id], - } - response = self.drive_service.files().create(body=file_metadata).execute() - folder_id: str = response.get("id") - return folder_id - - async def upload_file( - self, - file_path: str, - file_name: str, - parent_folder_id: str, - mimetype: str = "application/pdf", - ) -> str: - file_metadata = { - "name": file_name, - "mimeType": mimetype, - "parents": [parent_folder_id], - } - media = googleapiclient.http.MediaFileUpload( - file_path, - mimetype=mimetype, - ) - response = ( - self.drive_service.files() - .create(body=file_metadata, media_body=media) - .execute() - ) - folder_id: str = response.get("id") - return folder_id - async def upload_team_file( self, file_path: str, file_name: str, db: AsyncSession, + settings: Settings, ) -> str: - await self.init_folders(db) + await self.init_folders(db, settings) if not self.drive_folders or not self.drive_folders.registering_folder_id: hyperion_error_logger.error("No registering folder id found in database") return "" - return await self.upload_file( - file_path, - file_name, - self.drive_folders.registering_folder_id, - ) + async with DriveGoogleAPI(db, settings) as google_api: + return await google_api.upload_file( + file_path, + file_name, + self.drive_folders.registering_folder_id, + ) async def upload_participant_file( self, file_path: str, file_name: str, db: AsyncSession, + settings: Settings, ) -> str: - await self.init_folders(db) + await self.init_folders(db, settings) if not self.drive_folders or not self.drive_folders.security_folder_id: hyperion_error_logger.error("No security folder id found in database") return "" - return await self.upload_file( - file_path, - file_name, - self.drive_folders.security_folder_id, - ) + async with DriveGoogleAPI(db, settings) as google_api: + return await google_api.upload_file( + file_path, + file_name, + self.drive_folders.security_folder_id, + ) async def upload_raid_file( self, file_path: str, file_name: str, db: AsyncSession, + settings: Settings, ) -> str: - await self.init_folders(db) + await self.init_folders(db, settings) if not self.drive_folders or not self.drive_folders.parent_folder_id: hyperion_error_logger.error("No parent folder id found in database") return "" - return await self.upload_file( - file_path, - file_name, - self.drive_folders.parent_folder_id, - mimetype="text/csv", - ) - - def replace_file( - self, - file_path: str, - file_id: str, - ) -> str: - file_metadata = { - "mimeType": "application/pdf", - } - media = googleapiclient.http.MediaFileUpload( - file_path, - mimetype="application/pdf", - ) - response = ( - self.drive_service.files() - .update(fileId=file_id, body=file_metadata, media_body=media) - .execute() - ) - result: str = response.get("id") - return result - - def delete_file(self, file_id: str) -> None: - self.drive_service.files().delete(fileId=file_id).execute() + async with DriveGoogleAPI(db, settings) as google_api: + return await google_api.upload_file( + file_path, + file_name, + self.drive_folders.parent_folder_id, + mimetype="text/csv", + ) diff --git a/app/modules/raid/utils/pdf/pdf_writer.py b/app/modules/raid/utils/pdf/pdf_writer.py index 06b617379..d4659e896 100644 --- a/app/modules/raid/utils/pdf/pdf_writer.py +++ b/app/modules/raid/utils/pdf/pdf_writer.py @@ -1,4 +1,5 @@ import io +import logging import pathlib from pathlib import Path from typing import cast @@ -28,6 +29,8 @@ templates = Jinja2Templates(directory="assets/templates") +hyperion_error_logger = logging.getLogger("hyperion.error") + def maximize_image( image_path: Path, @@ -429,13 +432,20 @@ def write_participant_security_file( autoescape=select_autoescape(["html"]), ) results_template = environment.get_template("template.html") + context = { **participant.__dict__, "information": { - "president": information.president.__dict__, - "rescue": information.rescue.__dict__, - "security_responsible": information.security_responsible.__dict__, - "volunteer_responsible": information.volunteer_responsible.__dict__, + "president": information.president.__dict__ + if information.president + else None, + "rescue": information.rescue.__dict__ if information.rescue else None, + "security_responsible": information.security_responsible.__dict__ + if information.security_responsible + else None, + "volunteer_responsible": information.volunteer_responsible.__dict__ + if information.volunteer_responsible + else None, }, "team_number": team_number, } diff --git a/app/modules/raid/utils/utils_raid.py b/app/modules/raid/utils/utils_raid.py index 3d967c717..a043c4b38 100644 --- a/app/modules/raid/utils/utils_raid.py +++ b/app/modules/raid/utils/utils_raid.py @@ -7,11 +7,13 @@ from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession +from app.core.config import Settings from app.core.payment import schemas_payment from app.modules.raid import coredata_raid, cruds_raid, models_raid, schemas_raid from app.modules.raid.schemas_raid import ParticipantBase, ParticipantUpdate from app.modules.raid.utils.drive.drive_file_manager import DriveFileManager from app.modules.raid.utils.pdf.pdf_writer import HTMLPDFWriter, PDFWriter +from app.utils.google_api.google_api import DriveGoogleAPI from app.utils.tools import ( get_core_data, ) @@ -90,6 +92,7 @@ async def write_teams_csv( teams: Sequence[models_raid.Team], db: AsyncSession, drive_file_manager: DriveFileManager, + settings: Settings, ) -> None: file_name = "Équipes - " + datetime.now(UTC).strftime("%Y-%m-%d_%H_%M_%S") + ".csv" file_path = "data/raid/" + file_name @@ -117,7 +120,12 @@ async def write_teams_csv( for line in data: await file.write(",".join(line) + "\n") - await drive_file_manager.upload_raid_file(file_path, file_name, db) + await drive_file_manager.upload_raid_file( + file_path, + file_name, + db, + settings=settings, + ) Path(file_path).unlink() @@ -136,6 +144,7 @@ async def save_team_info( team: models_raid.Team, db: AsyncSession, drive_file_manager: DriveFileManager, + settings: Settings, ) -> None: try: pdf_writer = PDFWriter() @@ -143,7 +152,8 @@ async def save_team_info( file_name = file_path.split("/")[-1] if team.file_id: try: - file_id = drive_file_manager.replace_file(file_path, team.file_id) + async with DriveGoogleAPI(db, settings) as google_api: + file_id = google_api.replace_file(file_path, team.file_id) except Exception: hyperion_error_logger.exception( "RAID: could not replace file", @@ -152,12 +162,14 @@ async def save_team_info( file_path, file_name, db, + settings=settings, ) else: file_id = await drive_file_manager.upload_team_file( file_path, file_name, db, + settings=settings, ) await cruds_raid.update_team_file_id(team.id, file_id, db) pdf_writer.clear_pdf() @@ -170,6 +182,7 @@ async def post_update_actions( team: models_raid.Team | None, db: AsyncSession, drive_file_manager: DriveFileManager, + settings: Settings, ) -> None: try: if team: @@ -177,8 +190,18 @@ async def post_update_actions( await set_team_number(team, db) all_teams = await cruds_raid.get_all_validated_teams(db) if all_teams: - await write_teams_csv(all_teams, db, drive_file_manager) - await save_team_info(team, db, drive_file_manager) + await write_teams_csv( + all_teams, + db, + drive_file_manager, + settings=settings, + ) + await save_team_info( + team, + db, + drive_file_manager, + settings=settings, + ) except Exception: hyperion_error_logger.exception("Error while creating pdf") return None @@ -190,6 +213,7 @@ async def save_security_file( team_number: int | None, db: AsyncSession, drive_file_manager: DriveFileManager, + settings: Settings, ) -> None: try: pdf_writer = HTMLPDFWriter() @@ -199,22 +223,24 @@ async def save_security_file( team_number, ) file_name = f"{str(team_number) + '_' if team_number else ''}{participant.firstname}_{participant.name}_fiche_sécurité.pdf" - if participant.security_file and participant.security_file.file_id: - file_id = drive_file_manager.replace_file( - file_path, - participant.security_file.file_id, - ) - else: - file_id = await drive_file_manager.upload_participant_file( - file_path, - file_name, + async with DriveGoogleAPI(db, settings) as google_api: + if participant.security_file and participant.security_file.file_id: + file_id = google_api.replace_file( + file_path, + participant.security_file.file_id, + ) + else: + file_id = await drive_file_manager.upload_participant_file( + file_path, + file_name, + db, + settings=settings, + ) + await cruds_raid.update_security_file_id( + participant.security_file.id, + file_id, db, ) - await cruds_raid.update_security_file_id( - participant.security_file.id, - file_id, - db, - ) Path(file_path).unlink() except Exception: hyperion_error_logger.exception("Error while creating pdf") diff --git a/app/utils/google_api/google_api.py b/app/utils/google_api/google_api.py index 1fb371aff..66cce24f4 100644 --- a/app/utils/google_api/google_api.py +++ b/app/utils/google_api/google_api.py @@ -34,11 +34,14 @@ class GoogleAPI: "https://www.googleapis.com/auth/drive.readonly", ] + def is_google_api_configured(self, settings: Settings) -> bool: + return ( + settings.GOOGLE_API_CLIENT_ID is not None + and settings.GOOGLE_API_CLIENT_SECRET is not None + ) + def _get_flow(self, settings: Settings) -> Flow: - if ( - settings.GOOGLE_API_CLIENT_ID is None - or settings.GOOGLE_API_CLIENT_SECRET is None - ): + if not self.is_google_api_configured(settings): raise GoogleAPIMissingConfigInDotenvError client_config = {