import datetime import decimal from typing import Any, Generic, TypeVar from audience_common.formatters import format_datetime from audience_common.types import UTCDateTime from pydantic import AnyUrl, BaseModel, Field, HttpUrl, validator from pydantic.generics import GenericModel from campaigns.assets.enums import AssetStatus from campaigns.auth.dtos import User from campaigns.campaigns.enums import CampaignStatus from campaigns.connectors.facebook.constants import CITY, COUNTRY, REGION from campaigns.connectors.facebook.models import ( TargetingCity, TargetingGeolocations, TargetingInterestShort, TargetingRegion, ) from campaigns.core.types import PlainToken from campaigns.meta.enums import ( AdFormatGroup, AssetPositionGroup, AudienceGenderSelector, CampaignCallToAction, CampaignContentType, CampaignLinkType, CampaignObjective, CampaignPlatform, CampaignPlatformSelector, CampaignSummaryId, FacebookConnectionStatus, ) from campaigns.meta.handlers import GetCampaignAudienceSummaryRequest def schema_extra(schema: dict[str, Any], model: BaseModel) -> None: for prop, value in schema.get("properties", {}).items(): field = [x for x in model.__fields__.values() if x.alias == prop][0] if field.allow_none: if "$ref" in value: if issubclass(field.type_, BaseModel): value["title"] = ( field.type_.__config__.title or field.type_.__name__ ) value["anyOf"] = [{"$ref": value.pop("$ref")}] value["nullable"] = True BaseModel.Config.schema_extra = schema_extra # type: ignore[assignment] BaseModel.Config.json_encoders = { datetime.datetime: format_datetime, } BaseModel.Config.allow_population_by_field_name = True # Common T = TypeVar("T") class LimitOffsetPage(GenericModel, Generic[T]): total: int items: list[T] limit: int offset: int # Infra class HelloResponse(BaseModel): status: str = "ok" # Errors class Error(BaseModel): code: str message: str # Assets class AssetError(BaseModel): actual: str code: str expected: list[str] class CreateAssetInput(BaseModel): filename: str = Field( regex=r"^[^/*?]+$", # anything except /,*,? max_length=256, example="asset.jpg", embed=True, ) class Asset(BaseModel): id: str status: AssetStatus filename: str url: str | None thumbnail_url: str | None = Field(alias="thumbnailUrl") size: int | None filetype: str | None content_type: str | None = Field(alias="contentType") errors: list[AssetError] | None meta: dict[str, Any] created_at: UTCDateTime = Field(alias="createdAt") updated_at: UTCDateTime = Field(alias="updatedAt") uploaded_at: UTCDateTime | None = Field(alias="uploadedAt") class Config: orm_mode = True class UpdateAssetInput(BaseModel): status: AssetStatus | None size: int | None filetype: str | None content_type: str | None = Field(alias="contentType") meta: dict[str, Any] | None errors: list[AssetError] | None uploaded_at: UTCDateTime | None = Field(alias="uploadedAt") class AssetUploadCredentials(BaseModel): asset_id: str = Field(alias="assetId") upload_bucket: str = Field(alias="uploadBucket") upload_key: str = Field(alias="uploadKey") upload_region: str = Field(alias="uploadRegion") access_key_id: str = Field(alias="accessKeyId") secret_access_key: str = Field(alias="secretAccessKey") session_token: str = Field(alias="sessionToken") expiration: datetime.datetime = Field() upload_metadata: dict[str, Any] = Field( default_factory=dict, alias="uploadMetadata" ) # Campaign class Artist(BaseModel): id: str class Campaign(BaseModel): id: str name: str vendor_id: int = Field(alias="vendorId") subaccount_id: int = Field(alias="subaccountId") global_participant_id: str = Field(alias="globalParticipantId") status: CampaignStatus start_time: UTCDateTime | None = Field(alias="startTime") end_time: UTCDateTime | None = Field(alias="endTime") lifetime_budget: decimal.Decimal | None = Field(alias="lifetimeBudget") objective: CampaignObjective platforms: list[CampaignPlatform] created_at: UTCDateTime = Field(alias="createdAt") created_by: str = Field(alias="createdBy") updated_at: UTCDateTime = Field(alias="updatedAt") updated_by: str = Field(alias="updatedBy") class Config: orm_mode = True @validator("platforms", pre=True, always=True) def set_platforms(cls, v: list[CampaignPlatform] | None) -> list[CampaignPlatform]: return v or [] # Facebook class FacebookUser(BaseModel): name: str picture: str | None granted_scopes: list[str] = Field(alias="grantedScopes") class FacebookConnection(BaseModel): user: FacebookUser | None status: FacebookConnectionStatus scopes: list[str] required_scopes: list[str] = Field(alias="requiredScopes") # TODO: DEPRECATED class ConnectFacebookUserInput(BaseModel): user_id: str = Field(alias="userId") access_token: PlainToken = Field(alias="accessToken") class FacebookPage(BaseModel): id: str external_id: str = Field(alias="externalId") name: str is_manageable: bool = Field(alias="isManageable") is_valid: bool = Field(alias="isValid") picture_url: str | None = Field(alias="pictureUrl") class Config: orm_mode = True class FacebookTargetingGeolocation(BaseModel): key: str name: str type: str country_code: str | None = Field(alias="countryCode") country_name: str | None = Field(alias="countryName") region: str | None class FacebookTargetingInterest(BaseModel): id: str name: str path: list[str] disambiguation_category: str | None = Field(alias="disambiguationCategory") topic: str | None class MetaEngagementAudienceEvent(BaseModel): id: str name: str platform: CampaignPlatform class MetaCampaignEngagementAudience(BaseModel): id: str lower_bound: int = Field(alias="lowerBound") upper_bound: int = Field(alias="upperBound") event_ids: list[str] = Field(alias="eventIds") class CreateMetaCampaignEngagementAudienceInput(BaseModel): event_ids: list[str] = Field( alias="eventIds", min_items=1, ) class InstagramAccount(BaseModel): id: str name: str | None username: str profile_picture_url: str | None = Field(alias="profilePictureUrl") class InstagramAccountResponse(BaseModel): instagram_account: InstagramAccount | None = Field(alias="instagramAccount") # Meta class MetaCampaignImage(BaseModel): id: str asset_id: str = Field(alias="assetId") position_group: AssetPositionGroup = Field(alias="positionGroup") filename: str url: str | None thumbnail_url: str | None = Field(alias="thumbnailUrl") size: int | None status: AssetStatus errors: list[AssetError] = Field(default_factory=list) created_at: UTCDateTime = Field(alias="createdAt") updated_at: UTCDateTime = Field(alias="updatedAt") uploaded_at: UTCDateTime | None = Field(alias="uploadedAt") class Config: orm_mode = True class MetaCampaignVideo(BaseModel): id: str asset_id: str = Field(alias="assetId") position_group: AssetPositionGroup = Field(alias="positionGroup") filename: str url: str | None thumbnail_url: str | None = Field(alias="thumbnailUrl") size: int | None status: AssetStatus errors: list[AssetError] = Field(default_factory=list) created_at: UTCDateTime = Field(alias="createdAt") updated_at: UTCDateTime = Field(alias="updatedAt") uploaded_at: UTCDateTime | None = Field(alias="uploadedAt") class Config: orm_mode = True class MetaCampaign(BaseModel): id: str name: str vendor_id: int = Field(alias="vendorId") subaccount_id: int = Field(alias="subaccountId") global_participant_id: str = Field(alias="globalParticipantId") product_id: int | None = Field(alias="productId") product_upc: str | None = Field(alias="productUpc") status: CampaignStatus facebook_page_id: str = Field(alias="facebookPageId") instagram_account_id: str | None = Field(alias="instagramAccountId") post_external_id: str | None = Field(alias="postExternalId") start_time: UTCDateTime | None = Field(alias="startTime") end_time: UTCDateTime | None = Field(alias="endTime") lifetime_budget: decimal.Decimal | None = Field(alias="lifetimeBudget") content_type: CampaignContentType = Field(alias="contentType") objective: CampaignObjective platforms: list[CampaignPlatform] primary_text: str | None = Field(alias="primaryText") headline: str | None description: str | None link_type: CampaignLinkType | None = Field(alias="linkType") website_url: str | None = Field(alias="websiteUrl") landing_page_path: str | None = Field( alias="landingPagePath", description="Songwhip landing page path" ) landing_page_product_id: int | None = Field( alias="landingPageProductId", description="Songwhip landing page product Id", ) call_to_action: CampaignCallToAction | None = Field(alias="callToAction") created_at: UTCDateTime = Field(alias="createdAt") created_by: str = Field(alias="createdBy") updated_at: UTCDateTime = Field(alias="updatedAt") updated_by: str = Field(alias="updatedBy") images: list[MetaCampaignImage] videos: list[MetaCampaignVideo] class Config: orm_mode = True @validator("platforms", pre=True, always=True) def set_platforms(cls, v: list[CampaignPlatform] | None) -> list[CampaignPlatform]: return v or [] class MetaCampaignError(BaseModel): field: str code: str message: str class MetaAdCampaign(BaseModel): status: CampaignStatus class MetaCampaignSummary(BaseModel): id: CampaignSummaryId errors: list[MetaCampaignError] class MetaCampaignOverview(BaseModel): campaign: MetaCampaign summary: list[MetaCampaignSummary] class MetaCampaignInput(BaseModel): name: str vendor_id: int = Field(alias="vendorId") subaccount_id: int | None = Field(alias="subaccountId") global_participant_id: str = Field(alias="globalParticipantId") product_id: int | None = Field(alias="productId") facebook_page_id: str = Field(alias="facebookPageId") objective: CampaignObjective = Field(alias="objective") class MetaCampaignScheduleAndBudgetInput(BaseModel): start_time: UTCDateTime = Field(alias="startTime") end_time: UTCDateTime = Field(alias="endTime") lifetime_budget: decimal.Decimal = Field(alias="lifetimeBudget", gt=0) class MetaCampaignCreativeInput(BaseModel): content_type: CampaignContentType = Field(alias="contentType") platform: CampaignPlatformSelector post_external_id: str | None = Field(alias="postExternalId") primary_text: str | None = Field(alias="primaryText") headline: str | None description: str | None link_type: CampaignLinkType = Field(alias="linkType") website_url: AnyUrl | None = Field(alias="websiteUrl") landing_page_path: str | None = Field( alias="landingPagePath", description="Songwhip landing page path" ) landing_page_product_id: int | None = Field( alias="landingPageProductId", description="Songwhip landing page product Id", ) call_to_action: CampaignCallToAction = Field(alias="callToAction") class MetaTargetingAudience(BaseModel): id: str locations: list[FacebookTargetingGeolocation] age_min: int = Field(alias="ageMin") age_max: int = Field(alias="ageMax") gender: AudienceGenderSelector interests: list[FacebookTargetingInterest] created_at: UTCDateTime = Field(alias="createdAt") updated_at: UTCDateTime = Field(alias="updatedAt") class Config: orm_mode = True class MetaTargetingAudienceInput(BaseModel): locations: list[FacebookTargetingGeolocation] = Field(min_items=1) age_min: int = Field(18, alias="ageMin", ge=13) age_max: int = Field(alias="ageMax", le=65) gender: AudienceGenderSelector interests: list[FacebookTargetingInterest] class MetaCampaignAudiences(BaseModel): targeting_audience: MetaTargetingAudience | None = Field(alias="targetingAudience") class CreateMetaImageInput(CreateAssetInput): position_group: AssetPositionGroup = Field(alias="positionGroup") class MetaCampaignImageCredentials(BaseModel): image_id: str = Field(alias="imageId") credentials: AssetUploadCredentials class CreateMetaVideoInput(CreateAssetInput): position_group: AssetPositionGroup = Field(alias="positionGroup") class MetaCampaignVideoCredentials(BaseModel): video_id: str = Field(alias="videoId") credentials: AssetUploadCredentials class MetaCampaignAdPreview(BaseModel): body: str src: str height: int width: int class MetaCampaignAdFormat(BaseModel): id: str name: str group: AdFormatGroup platform: CampaignPlatformSelector class CreateMetaAdImageInput(BaseModel): url: HttpUrl class CreateMetaAdVideoInput(BaseModel): url: HttpUrl class MetaCampaignAudienceSummary(BaseModel): users_lower_bound: int = Field(alias="usersLowerBound") users_upper_bound: int = Field(alias="usersUpperBound") estimate_ready: bool = Field(alias="estimateReady") class MetaAudienceSummaryInput(BaseModel): locations: list[FacebookTargetingGeolocation] | None interests: list[FacebookTargetingInterest] | None gender: AudienceGenderSelector | None age_min: int | None = Field(alias="ageMin") age_max: int | None = Field(alias="ageMax") def to_request( self, user: User, campaign_id: str ) -> GetCampaignAudienceSummaryRequest: if self.locations: countries = [ location.key for location in self.locations if location.type == COUNTRY ] regions = [ TargetingRegion(key=location.key) for location in self.locations if location.type == REGION ] cities = [ TargetingCity(key=location.key) for location in self.locations if location.type == CITY ] geo_locations = TargetingGeolocations( countries=countries, regions=regions, cities=cities, ) else: geo_locations = None if self.interests: interests = [ TargetingInterestShort(id=interest.id, name=interest.name) for interest in self.interests ] else: interests = None if self.gender: genders = [gender.int_value for gender in self.gender.to_list()] else: genders = None return GetCampaignAudienceSummaryRequest( user=user, campaign_id=campaign_id, geo_locations=geo_locations, interests=interests, genders=genders, age_min=self.age_min, age_max=self.age_max, ) class MetaCampaignPost(BaseModel): id: str platform: CampaignPlatform type: str title: str | None permalink_url: str = Field(alias="permalinkUrl") picture_url: str | None = Field(alias="pictureUrl") thumbnail_url: str | None = Field(alias="thumbnailUrl") created_at: UTCDateTime = Field(alias="createdAt") class MetaCampaignPostResponse(BaseModel): post: MetaCampaignPost | None # Test class CreateTestFacebookPageInput(BaseModel): association__orchard_identity_id: str | None = Field(alias="orchardIdentityId") class CreateTestMetaCampaignInput(BaseModel): name: str | None status: CampaignStatus | None vendor_id: int | None = Field(alias="vendorId") subaccount_id: int | None = Field(alias="subaccountId") global_participant_id: str | None = Field(alias="globalParticipantId") created_by: str | None = Field(alias="createdBy")