from typing import Annotated, Any from anydi.ext.fastapi import Inject from fansifter_common.api.schemas import response_errors from fansifter_common.artist.exceptions import InvalidGlobalParticipantIdError from fansifter_common.auth.identity import Identity from fansifter_common.utils import timezone from fastapi import APIRouter, Depends, Path, Query from starlette import status from dmp.api import auth from dmp.api.audiences import schemas from dmp.api.schemas import ArtistWithId, LimitOffsetPage from dmp.audiences.exceptions import ( ArtistPhoneNumberNotFoundError, AudienceCityFilterError, AudienceExportNotFoundError, AudienceNotFoundError, AudienceShareNotFoundError, AudienceTargetNotSupportedError, ) from dmp.audiences.handlers import ( ArchiveAudienceHandler, ArchiveAudienceRequest, CreateAudienceHandler, CreateAudienceRequest, ExportAudienceHandler, ExportAudienceRequest, GetAudienceAdAccountsHandler, GetAudienceAdAccountsRequest, GetAudienceExportHandler, GetAudienceExportRequest, GetAudienceHandler, GetAudienceRequest, GetAudiencesArtistsHandler, GetAudiencesArtistsRequest, GetAudiencesHandler, GetAudienceSizeHandler, GetAudienceSizeRequest, GetAudiencesRequest, RefreshAudienceHandler, RefreshAudienceRequest, UpdateAudienceExportHandler, UpdateAudienceExportRequest, UpdateAudienceHandler, UpdateAudienceRequest, UpdateAudienceShareHandler, UpdateAudienceShareRequest, UpsertAudienceFansHandler, UpsertAudienceFansRequest, ) from dmp.audiences.models import Audience, AudienceExport, AudienceShare from dmp.rosters.exceptions import ( FanDataListIdIsRequiredError, MainRepArtistOnlyAllowedError, ) router = APIRouter(tags=["Audiences"]) @router.get( "/audiences", operation_id="getAudiences", response_model=LimitOffsetPage[schemas.Audience], response_model_by_alias=True, ) def get_audiences( identity: Annotated[Identity, Depends(auth.get_identity)], data: Annotated[schemas.GetAudiencesInput, Query()], handler: Annotated[GetAudiencesHandler, Inject()], ) -> Any: limit = data.limit or GetAudiencesRequest.DEFAULT_LIMIT offset = data.offset or GetAudiencesRequest.DEFAULT_OFFSET order_by = data.order_by or GetAudiencesRequest.DEFAULT_ORDER_BY response = handler.handle( GetAudiencesRequest( identity=identity, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, search=data.search, global_participant_id=data.global_participant_id, custom_list_id=data.custom_list_id, audience_ids=data.audience_ids, target=data.target, include_archived=data.include_archived, order_by=order_by, limit=limit, offset=offset, ) ) return LimitOffsetPage( items=response.items, total=response.total, limit=limit, offset=offset, ) @router.post( "/audiences", operation_id="createAudience", response_model=schemas.Audience, response_model_by_alias=True, status_code=status.HTTP_201_CREATED, responses=response_errors( InvalidGlobalParticipantIdError, FanDataListIdIsRequiredError, MainRepArtistOnlyAllowedError, AudienceCityFilterError, ArtistPhoneNumberNotFoundError, ), ) def create_audience( identity: Annotated[Identity, Depends(auth.get_identity)], data: schemas.CreateAudienceInput, handler: Annotated[CreateAudienceHandler, Inject()], ) -> Audience: return handler.handle( CreateAudienceRequest( identity=identity, name=data.name, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, fandata_list_ids=data.fandata_list_ids, target=data.target, filters=data.filters, ) ) @router.get( "/audiences/{audienceId}", operation_id="getAudience", response_model=schemas.Audience, response_model_by_alias=True, responses=response_errors( AudienceNotFoundError, ), ) def get_audience( identity: Annotated[Identity, Depends(auth.get_identity)], audience_id: Annotated[str, Path(alias="audienceId")], handler: Annotated[GetAudienceHandler, Inject()], ) -> Audience: return handler.handle( GetAudienceRequest( identity=identity, audience_id=audience_id, ) ) @router.put( "/audiences/{audienceId}", operation_id="updateAudience", response_model=schemas.Audience, response_model_by_alias=True, responses=response_errors( AudienceNotFoundError, MainRepArtistOnlyAllowedError, AudienceCityFilterError, ArtistPhoneNumberNotFoundError, ), ) def update_audience( identity: Annotated[Identity, Depends(auth.get_identity)], audience_id: Annotated[str, Path(alias="audienceId")], data: schemas.UpdateAudienceInput, handler: Annotated[UpdateAudienceHandler, Inject()], ) -> Audience: return handler.handle( UpdateAudienceRequest( identity=identity, audience_id=audience_id, name=data.name, target=data.target, filters=data.filters, ) ) @router.post( "/audiences/{audienceId}/refresh", operation_id="refreshAudience", response_model=schemas.Audience, response_model_by_alias=True, responses=response_errors( AudienceNotFoundError, ), ) def refresh_audience( identity: Annotated[Identity, Depends(auth.get_identity)], audience_id: Annotated[str, Path(alias="audienceId")], handler: Annotated[RefreshAudienceHandler, Inject()], ) -> Audience: return handler.handle( RefreshAudienceRequest( identity=identity, audience_id=audience_id, ) ) @router.post( "/audiences/{audienceId}/fans/upsert", operation_id="upsertAudienceFans", tags=["internal"], response_model=schemas.CreateTargetAudienceOutput, responses=response_errors( AudienceNotFoundError, AudienceTargetNotSupportedError, ), ) def upsert_audience_fans( audience_id: Annotated[str, Path(alias="audienceId")], handler: Annotated[UpsertAudienceFansHandler, Inject()], ) -> Any: fan_count = handler.handle(UpsertAudienceFansRequest(audience_id=audience_id)) return {"fan_count": fan_count} @router.post( "/audiences/{audienceId}/archive", operation_id="archiveAudience", status_code=status.HTTP_204_NO_CONTENT, responses=response_errors( AudienceNotFoundError, ), ) def archive_audience( identity_id: auth.IdentityId, audience_id: Annotated[str, Path(alias="audienceId")], handler: Annotated[ArchiveAudienceHandler, Inject()], ) -> None: handler.handle( ArchiveAudienceRequest( identity_id=identity_id, audience_id=audience_id, ) ) @router.post( "/audiences/{audienceId}/exports", operation_id="exportAudience", response_model=schemas.AudienceExport, response_model_by_alias=True, status_code=status.HTTP_201_CREATED, responses=response_errors( AudienceNotFoundError, ), ) def export_audience( identity_id: auth.IdentityId, audience_id: Annotated[str, Path(alias="audienceId")], data: schemas.ExportAudienceInput, handler: Annotated[ExportAudienceHandler, Inject()], ) -> Any: return handler.handle( ExportAudienceRequest( identity_id=identity_id, audience_id=audience_id, reason=data.reason, reason_notes=data.reason_notes, justification=data.justification, ) ) @router.get( "/audiences/{audienceId}/exports/{exportId}", operation_id="getAudienceExport", response_model=schemas.AudienceExportOutput, response_model_by_alias=True, responses=response_errors( AudienceExportNotFoundError, ), ) def get_audience_export( identity_id: auth.IdentityId, audience_id: Annotated[str, Path(alias="audienceId")], export_id: Annotated[str, Path(alias="exportId")], handler: Annotated[GetAudienceExportHandler, Inject()], ) -> Any: response = handler.handle( GetAudienceExportRequest( identity_id=identity_id, audience_id=audience_id, export_id=export_id, ) ) return { "export": response.export, "presignedUrl": response.presigned_url, } @router.get( "/audiences/{audienceId}/ad-accounts", operation_id="getAudienceAdAccounts", response_model=list[schemas.AudienceAdAccount], responses=response_errors( AudienceNotFoundError, ), ) def get_audience_ad_accounts( identity_id: auth.IdentityId, audience_id: Annotated[str, Path(alias="audienceId")], handler: Annotated[GetAudienceAdAccountsHandler, Inject()], ) -> Any: return handler.handle( GetAudienceAdAccountsRequest(identity_id=identity_id, audience_id=audience_id) ) @router.post( "/audience-size", operation_id="getAudienceSize", response_model=schemas.AudienceSizeOutput, response_model_by_alias=True, responses=response_errors( FanDataListIdIsRequiredError, MainRepArtistOnlyAllowedError, ArtistPhoneNumberNotFoundError, ), ) def get_audience_size( identity: Annotated[Identity, Depends(auth.get_identity)], data: schemas.GetAudienceSizeInput, handler: Annotated[GetAudienceSizeHandler, Inject()], ) -> dict[str, Any]: size = handler.handle( GetAudienceSizeRequest( identity=identity, vendor_id=data.vendor_id, subaccount_id=data.subaccount_id, fandata_list_ids=data.fandata_list_ids, target=data.target, filters=data.filters, ) ) return {"size": size, "createdAt": timezone.now()} @router.get( "/audiences-artists", operation_id="getAudiencesArtists", deprecated=True, description="Get artists list for already created audiences", response_model=list[ArtistWithId], ) def get_audiences_artists( identity_id: auth.IdentityId, handler: Annotated[GetAudiencesArtistsHandler, Inject()], ) -> Any: artist_ids = handler.handle(GetAudiencesArtistsRequest(identity_id=identity_id)) return [{"id": artist_id} for artist_id in artist_ids] # Internal @router.patch( "/audience-exports/{exportId}", tags=["internal"], operation_id="updateAudienceExport", response_model=schemas.AudienceExport, responses=response_errors( AudienceExportNotFoundError, ), ) def update_audience_export( export_id: Annotated[str, Path(alias="exportId")], data: schemas.UpdateAudienceExportInput, handler: Annotated[UpdateAudienceExportHandler, Inject()], ) -> AudienceExport: return handler.handle( UpdateAudienceExportRequest( export_id=export_id, status=data.status, zip_key=data.zip_key, ) ) @router.patch( "/audience-shares/{shareId}", tags=["internal"], operation_id="updateAudienceShare", response_model=schemas.AudienceShare, responses=response_errors( AudienceShareNotFoundError, ), ) def update_audience_share( share_id: Annotated[str, Path(alias="shareId")], data: schemas.UpdateAudienceShareInput, handler: Annotated[UpdateAudienceShareHandler, Inject()], ) -> AudienceShare: return handler.handle( UpdateAudienceShareRequest( share_id=share_id, status=data.status, processed_fans=data.processed_fans, ) )