from collections.abc import Iterable from typing import Annotated, Any from anydi.ext.fastapi import Inject from fansifter_common.api.schemas import response_errors from fastapi import APIRouter, Body, Depends, Path, Query from starlette import status from starlette.responses import Response from dmp.api import auth from dmp.api.google import schemas from dmp.api.google.schemas import GetGoogleAdAccountsInput from dmp.api.schemas import ( AdReportingConnectionCredentials, LimitOffsetPage, SyncFivetranTablesStateInput, ) from dmp.app_connections.exceptions import AppConnectionNotFoundError from dmp.audiences.exceptions import ( AudienceShareActiveConnectionNotFoundError, AudienceShareError, AudienceTooSmallError, EmptyAudienceError, ) from dmp.google.dtos import AssignGoogleUserAdAccountLabelData, GoogleUserAdAccountLabel from dmp.google.exceptions import ( GoogleAdAccountNotFoundError, GoogleAdReportingConnectionNotFoundError, GoogleAudienceNotFoundError, GoogleUserAdAccountNotFoundError, GoogleUserConnectionNotFoundError, InvalidGoogleAdAccountId, ) from dmp.google.handlers import ( AssignLabelsToGoogleUserAdAccountsHandler, AssignLabelsToGoogleUserAdAccountsRequest, ConnectGoogleAdReportingHandler, ConnectGoogleAdReportingRequest, ConnectGoogleUserHandler, ConnectGoogleUserRequest, DeleteGoogleAdAccountsHandler, DeleteGoogleAdAccountsRequest, DeleteGoogleUserAdAccountHandler, DeleteGoogleUserAdAccountRequest, GetGoogleAdAccountsHandler, GetGoogleAdAccountsRequest, GetGoogleAdAccountsV2Handler, GetGoogleAdAccountsV2Request, GetGoogleAdReportingConnectionHandler, GetGoogleAdReportingConnectionRequest, GetGoogleAudienceShareHandler, GetGoogleAudienceShareRequest, GetGoogleAudienceShareResponse, GetGoogleAudienceShareStatusHandler, GetGoogleAudienceShareStatusRequest, GetGoogleAudienceShareStatusResponse, GetGoogleFivetranConnectionsHandler, GetGoogleUserAdAccountsHandler, GetGoogleUserAdAccountsRequest, GetGoogleUserConnectionHandler, GetGoogleUserConnectionRequest, RefreshGoogleAdReportingConnectionHandler, RefreshGoogleAdReportingConnectionRequest, ShareGoogleAudienceHandler, ShareGoogleAudienceRequest, ) from dmp.google.handlers.notify_ad_reporting_sync_completed import ( NotifyGoogleAdReportingSyncCompletedHandler, ) from dmp.google.handlers.sync_fivetran_tables import ( SyncGoogleFivetranTablesStateHandler, SyncGoogleFivetranTablesStateRequest, ) from dmp.google.models import GoogleAudience router = APIRouter(prefix="/google", tags=["Google"]) @router.post( "/user/connection", operation_id="connectGoogleUser", response_model=schemas.GoogleUserConnection, response_model_by_alias=True, ) def connect_google_user( identity_id: auth.IdentityId, data: Annotated[schemas.ConnectGoogleUserInput, Body()], handler: Annotated[ConnectGoogleUserHandler, Inject()], ) -> Any: return handler.handle( ConnectGoogleUserRequest( identity_id=identity_id, auth_code=data.auth_code, redirect_uri=data.redirect_uri, code_verifier=data.code_verifier, ) ) @router.get( "/user/connection", operation_id="getGoogleUserConnection", response_model=schemas.GoogleUserConnection, ) def get_google_user_connection( identity_id: auth.IdentityId, data: Annotated[schemas.GoogleUserInput, Depends()], handler: Annotated[GetGoogleUserConnectionHandler, Inject()], ) -> Any: return handler.handle( GetGoogleUserConnectionRequest(identity_id=identity_id, user_id=data.user_id) ) @router.get( "/user/ad-accounts", operation_id="getGoogleUserAdAccounts", response_model=list[schemas.GoogleUserAdAccount], responses=response_errors( GoogleUserConnectionNotFoundError, ), ) def get_user_ad_accounts( identity_id: auth.IdentityId, data: Annotated[schemas.GoogleUserInput, Depends()], handler: Annotated[GetGoogleUserAdAccountsHandler, Inject()], ) -> list[GoogleUserAdAccountLabel]: return handler.handle( GetGoogleUserAdAccountsRequest( identity_id=identity_id, user_id=data.user_id, ) ) @router.post( "/user/ad-accounts", operation_id="assignLabelsToGoogleUserAdAccounts", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, ) def assign_labels_to_user_ad_accounts( identity_id: auth.IdentityId, data: Annotated[schemas.AssignLabelsToGoogleUserAdAccountsInput, Body()], handler: Annotated[AssignLabelsToGoogleUserAdAccountsHandler, Inject()], ) -> None: handler.handle( AssignLabelsToGoogleUserAdAccountsRequest( identity_id=identity_id, assignments=[ AssignGoogleUserAdAccountLabelData( ad_account_id=assignment.ad_account_id, vendor_id=assignment.vendor_id, subaccount_id=assignment.subaccount_id, ) for assignment in data ], ) ) return None @router.delete( "/user/ad-accounts/{adAccountId}", operation_id="deleteGoogleUserAdAccount", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( GoogleAdAccountNotFoundError, ), ) def delete_google_user_ad_account( identity_id: auth.IdentityId, ad_account_id: Annotated[str, Path(alias="adAccountId")], handler: Annotated[DeleteGoogleUserAdAccountHandler, Inject()], ) -> None: handler.handle( DeleteGoogleUserAdAccountRequest( identity_id=identity_id, ad_account_id=ad_account_id ) ) @router.post( "/audiences", operation_id="shareGoogleAudience", response_model=schemas.GoogleAudience, responses=response_errors( EmptyAudienceError, InvalidGoogleAdAccountId, AudienceShareError, AudienceTooSmallError, AudienceShareActiveConnectionNotFoundError, ), status_code=status.HTTP_201_CREATED, ) def share_google_audience( identity_id: auth.IdentityId, data: Annotated[schemas.ShareGoogleAudienceInput, Body()], handler: Annotated[ShareGoogleAudienceHandler, Inject()], ) -> GoogleAudience: return handler.handle( ShareGoogleAudienceRequest( identity_id=identity_id, audience_id=data.audience_id, ad_account_id=data.ad_account_id, justification=data.justification, reason_notes=data.reason_notes, ) ) @router.get( "/audience-shares/{shareId}", tags=["internal"], operation_id="getGoogleAudienceShare", response_model=schemas.GoogleAudienceShare, responses=response_errors( GoogleAudienceNotFoundError, GoogleUserConnectionNotFoundError, ), ) def get_google_audience_share( share_id: Annotated[str, Path(alias="shareId")], handler: Annotated[GetGoogleAudienceShareHandler, Inject()], ) -> GetGoogleAudienceShareResponse: return handler.handle(GetGoogleAudienceShareRequest(share_id=share_id)) @router.get( "/audiences/{googleAudienceId}/share-status", operation_id="getGoogleAudienceShareStatus", response_model=schemas.GoogleAudienceShareStatus, responses=response_errors( GoogleAudienceNotFoundError, ), ) def get_google_audience_share_status( identity_id: auth.IdentityId, google_audience_id: Annotated[str, Path(alias="googleAudienceId")], handler: Annotated[GetGoogleAudienceShareStatusHandler, Inject()], ) -> GetGoogleAudienceShareStatusResponse: return handler.handle( GetGoogleAudienceShareStatusRequest( identity_id=identity_id, google_audience_id=google_audience_id ) ) @router.post( "/ad-reporting/connection", operation_id="connectGoogleAdReporting", response_model=AdReportingConnectionCredentials, response_model_by_alias=True, ) def connect_google_ad_reporting( identity_id: auth.IdentityId, data: Annotated[schemas.ConnectGoogleAdReportingInput, Body()], handler: Annotated[ConnectGoogleAdReportingHandler, Inject()], ) -> dict[str, Any]: connection, connect_card = handler.handle( ConnectGoogleAdReportingRequest( identity_id=identity_id, redirect_uri=str(data.redirect_uri), user_id=str(data.user_id), ) ) return { "connectionId": connection.id, "connectCardUri": connect_card.uri, } @router.get( "/ad-reporting/connection", operation_id="GetGoogleAdReportingConnection", response_model=schemas.GoogleAdReportingConnection, response_model_by_alias=True, ) def get_google_ad_reporting_connection( identity_id: auth.IdentityId, data: Annotated[schemas.GoogleUserInput, Depends()], handler: Annotated[GetGoogleAdReportingConnectionHandler, Inject()], ) -> Any: return handler.handle( GetGoogleAdReportingConnectionRequest( identity_id=identity_id, user_id=data.user_id, ) ) @router.put( "/ad-reporting/connection/refresh", operation_id="refreshGoogleAdReportingConnection", tags=["internal"], summary="Refresh Google Ad Reporting connection. Internal use only.", response_class=Response, status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( GoogleAdReportingConnectionNotFoundError, ), ) def refresh_google_ad_reporting_connection( data: Annotated[schemas.RefreshGoogleAdReportingConnectionInput, Body()], handler: Annotated[RefreshGoogleAdReportingConnectionHandler, Inject()], ) -> None: handler.handle( RefreshGoogleAdReportingConnectionRequest( fivetran_connection_id=data.fivetran_connection_id ) ) @router.get( "/ad-accounts", operation_id="getGoogleAdAccounts", response_model=list[schemas.GoogleAdAccount], ) def get_google_ad_accounts( identity_id: auth.IdentityId, handler: Annotated[GetGoogleAdAccountsHandler, Inject()], ) -> Any: return handler.handle(GetGoogleAdAccountsRequest(identity_id=identity_id)) @router.get( "/ad-accounts-v2", operation_id="getGoogleAdAccountsV2", response_model=LimitOffsetPage[schemas.GoogleAdAccount], response_model_by_alias=True, ) def get_google_ad_accounts_v2( identity_id: auth.IdentityId, handler: Annotated[GetGoogleAdAccountsV2Handler, Inject()], data: Annotated[GetGoogleAdAccountsInput, Query()], ) -> Any: limit = data.limit or GetGoogleAdAccountsV2Request.DEFAULT_LIMIT offset = data.offset or GetGoogleAdAccountsV2Request.DEFAULT_OFFSET order_by = data.order_by or GetGoogleAdAccountsV2Request.DEFAULT_ORDER_BY response = handler.handle( GetGoogleAdAccountsV2Request( identity_id=identity_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, limit=limit, offset=offset, order_by=order_by, ), ) return LimitOffsetPage( items=response.items, total=response.total, limit=limit, offset=offset, ) @router.delete( "/ad-accounts/{adAccountId}", operation_id="deleteGoogleAdAccount", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( GoogleAdAccountNotFoundError, GoogleUserAdAccountNotFoundError, ), ) def delete_google_ad_account( identity_id: auth.IdentityId, ad_account_id: Annotated[str, Path(alias="adAccountId")], data: Annotated[schemas.DeleteGoogleAdAccountsInput, Depends()], handler: Annotated[DeleteGoogleAdAccountsHandler, Inject()], ) -> None: handler.handle( DeleteGoogleAdAccountsRequest( identity_id=identity_id, ad_account_id=ad_account_id, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, ) ) @router.post( "/sync-fivetran-tables-state", operation_id="syncGoogleFivetranTablesState", summary="Sync Google Fivetran tables state for particular connection or all at once. Internal use only.", tags=["internal"], response_model=Iterable[str], responses=response_errors( AppConnectionNotFoundError, ), ) def handle_sync_google_fivetran_tables_state( data: SyncFivetranTablesStateInput, handler: Annotated[SyncGoogleFivetranTablesStateHandler, Inject()], ) -> Any: return handler.handle( SyncGoogleFivetranTablesStateRequest( fivetran_connection_id=data.fivetran_connection_id if data else None ), ) @router.post( "/ad-reporting/handle-dbt-sync", operation_id="handleGoogleAdReportingDbtSync", tags=["internal"], response_class=Response, status_code=status.HTTP_204_NO_CONTENT, ) def handle_ad_reporting_dbt_sync( handler: Annotated[NotifyGoogleAdReportingSyncCompletedHandler, Inject()], ) -> None: handler.handle() return None @router.get( "/fivetran-connectors", operation_id="getGoogleFivetranConnectorsIds", summary="Get all Fivetran connectors ids for Google connections. Internal use only.", tags=["internal"], response_model=Iterable[str], ) def handle_get_meta_fivetran_connectors_ids( handler: Annotated[GetGoogleFivetranConnectionsHandler, Inject()], ) -> Any: return handler.handle()