diff --git a/airbyte-integrations/connectors/source-bing-ads/acceptance-test-config.yml b/airbyte-integrations/connectors/source-bing-ads/acceptance-test-config.yml index 480c522ac5a87..c61a97075ea31 100644 --- a/airbyte-integrations/connectors/source-bing-ads/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-bing-ads/acceptance-test-config.yml @@ -9,6 +9,8 @@ acceptance_tests: discovery: tests: - config_path: secrets/config.json + backward_compatibility_tests_config: + disable_for_version: "1.4.0" connection: tests: - config_path: secrets/config.json diff --git a/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl index a4154ed8d18f1..62627cf0aeae4 100644 --- a/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-bing-ads/integration_tests/expected_records.jsonl @@ -1,5 +1,5 @@ {"stream":"ad_groups","data":{"AdRotation":null,"AudienceAdsBidAdjustment":null,"BiddingScheme":{"Type":"InheritFromParent","InheritedBidStrategyType":"EnhancedCpc"},"CpcBid":{"Amount":2.27},"EndDate":null,"FinalUrlSuffix":null,"ForwardCompatibilityMap":null,"Id":1356799861840328,"Language":null,"Name":"keywords","Network":"OwnedAndOperatedAndSyndicatedSearch","PrivacyStatus":null,"Settings":null,"StartDate":{"Day":7,"Month":11,"Year":2023},"Status":"Active","TrackingUrlTemplate":null,"UrlCustomParameters":null,"AdScheduleUseSearcherTimeZone":false,"AdGroupType":"SearchStandard","CpvBid":{"Amount":null},"CpmBid":{"Amount":null},"CampaignId":531016227,"AccountId":180519267,"CustomerId":251186883},"emitted_at":1699913367220} -{"stream":"ads","data":{"AdFormatPreference":"All","DevicePreference":0,"EditorialStatus":"Active","FinalAppUrls":null,"FinalMobileUrls":null,"FinalUrlSuffix":null,"FinalUrls":{"string":["https://airbyte.com"]},"ForwardCompatibilityMap":null,"Id":84800390693061,"Status":"Active","TrackingUrlTemplate":null,"Type":"ResponsiveSearch","UrlCustomParameters":null,"Descriptions":{"AssetLink":[{"Asset":{"Id":10239363892977,"Name":null,"Type":"TextAsset","Text":"Connect, integrate, and sync data seamlessly with Airbyte's 800+ contributors and growing!"},"AssetPerformanceLabel":"Learning","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892976,"Name":null,"Type":"TextAsset","Text":"Move data like a pro with our powerful tool trusted by 40,000+ engineers worldwide!"},"AssetPerformanceLabel":"Learning","EditorialStatus":"Active","PinnedField":null}]},"Domain":"airbyte.com","Headlines":{"AssetLink":[{"Asset":{"Id":10239363892979,"Name":null,"Type":"TextAsset","Text":"Get synced with Airbyte"},"AssetPerformanceLabel":"Good","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363893384,"Name":null,"Type":"TextAsset","Text":"Data management made easy"},"AssetPerformanceLabel":"Low","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892978,"Name":null,"Type":"TextAsset","Text":"Connectors for every need"},"AssetPerformanceLabel":"Best","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892980,"Name":null,"Type":"TextAsset","Text":"Industry-leading connectors"},"AssetPerformanceLabel":"Best","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363893383,"Name":null,"Type":"TextAsset","Text":"Try Airbyte now for free"},"AssetPerformanceLabel":"Good","EditorialStatus":"Active","PinnedField":null}]},"Path1":null,"Path2":null,"AdGroupId":1356799861840328,"AccountId":180519267,"CustomerId":251186883},"emitted_at":1699913377498} +{"stream":"ads","data":{"AdFormatPreference":"All","DevicePreference":0,"EditorialStatus":"Active","FinalAppUrls":null,"FinalMobileUrls":null,"FinalUrlSuffix":null,"FinalUrls":{"string":["https://airbyte.com"]},"ForwardCompatibilityMap":null,"Id":84800390693061,"Status":"Active","TrackingUrlTemplate":null,"Type":"ResponsiveSearch","UrlCustomParameters":null,"Descriptions":{"AssetLink":[{"Asset":{"Id":10239363892977,"Name":null,"Type":"TextAsset","Text":"Connect, integrate, and sync data seamlessly with Airbyte's 800+ contributors and growing!"},"AssetPerformanceLabel":"Learning","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892976,"Name":null,"Type":"TextAsset","Text":"Move data like a pro with our powerful tool trusted by 40,000+ engineers worldwide!"},"AssetPerformanceLabel":"Learning","EditorialStatus":"Active","PinnedField":null}]},"Domain":"airbyte.com","Headlines":{"AssetLink":[{"Asset":{"Id":10239363892979,"Name":null,"Type":"TextAsset","Text":"Get synced with Airbyte"},"AssetPerformanceLabel":"Good","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363893384,"Name":null,"Type":"TextAsset","Text":"Data management made easy"},"AssetPerformanceLabel":"Good","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892978,"Name":null,"Type":"TextAsset","Text":"Connectors for every need"},"AssetPerformanceLabel":"Best","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363892980,"Name":null,"Type":"TextAsset","Text":"Industry-leading connectors"},"AssetPerformanceLabel":"Best","EditorialStatus":"Active","PinnedField":null},{"Asset":{"Id":10239363893383,"Name":null,"Type":"TextAsset","Text":"Try Airbyte now for free"},"AssetPerformanceLabel":"Good","EditorialStatus":"Active","PinnedField":null}]},"Path1":null,"Path2":null,"AdGroupId":1356799861840328,"AccountId":180519267,"CustomerId":251186883},"emitted_at":1700075716309} {"stream":"campaigns","data":{"AudienceAdsBidAdjustment":0,"BiddingScheme":{"Type":"EnhancedCpc"},"BudgetType":"DailyBudgetStandard","DailyBudget":2.0,"ExperimentId":null,"FinalUrlSuffix":null,"ForwardCompatibilityMap":null,"Id":531016227,"MultimediaAdsBidAdjustment":40,"Name":"Airbyte test","Status":"Active","SubType":null,"TimeZone":"CentralTimeUSCanada","TrackingUrlTemplate":null,"UrlCustomParameters":null,"CampaignType":"Search","Settings":{"Setting":[{"Type":"TargetSetting","Details":{"TargetSettingDetail":[{"CriterionTypeGroup":"Audience","TargetAndBid":false}]}}]},"BudgetId":null,"Languages":{"string":["English"]},"AdScheduleUseSearcherTimeZone":false,"AccountId":180519267,"CustomerId":251186883},"emitted_at":1699913381852} {"stream":"accounts","data":{"BillToCustomerId":251186883,"CurrencyCode":"USD","AccountFinancialStatus":"ClearFinancialStatus","Id":180535609,"Language":"English","LastModifiedByUserId":0,"LastModifiedTime":"2023-08-11T08:24:26.603000","Name":"DEMO-ACCOUNT","Number":"F149W3B6","ParentCustomerId":251186883,"PaymentMethodId":null,"PaymentMethodType":null,"PrimaryUserId":138225488,"AccountLifeCycleStatus":"Pause","TimeStamp":"AAAAAH10c1A=","TimeZone":"Santiago","PauseReason":2,"ForwardCompatibilityMap":null,"LinkedAgencies":null,"SalesHouseCustomerId":null,"TaxInformation":null,"BackUpPaymentInstrumentId":null,"BillingThresholdAmount":null,"BusinessAddress":{"City":"San Francisco","CountryCode":"US","Id":149694999,"Line1":"350 29th avenue","Line2":null,"Line3":null,"Line4":null,"PostalCode":"94121","StateOrProvince":"CA","TimeStamp":null,"BusinessName":"Daxtarity Inc."},"AutoTagType":"Inactive","SoldToPaymentInstrumentId":null,"AccountMode":"Expert"},"emitted_at":1699913384475} {"stream":"account_performance_report_daily","data":{"AccountId":180519267,"TimePeriod":"2023-11-07","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Phrase","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","AccountNumber":"F149MJ18","PhoneImpressions":0,"PhoneCalls":0,"Clicks":1,"Ctr":16.67,"Spend":0.33,"Impressions":6,"CostPerConversion":null,"Ptr":null,"Assists":0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"AverageCpc":0.33,"AveragePosition":0.0,"AverageCpm":55.0,"Conversions":0.0,"ConversionRate":0.0,"ConversionsQualified":0.0,"LowQualityClicks":0,"LowQualityClicksPercent":0.0,"LowQualityImpressions":0,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1699953356703} @@ -8,15 +8,15 @@ {"stream":"ad_group_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-11-05","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","Language":"English","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","AdGroupType":"Standard","Impressions":3,"Clicks":0,"Ctr":0.0,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"CostPerAssist":null,"CustomParameters":null,"FinalUrlSuffix":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null,"HistoricalQualityScore":5.0,"HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":1.0},"emitted_at":1699953500072} {"stream":"ad_group_impression_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-07","Status":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":1,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"DeviceType":"Computer","Language":"German","ImpressionSharePercent":null,"ImpressionLostToBudgetPercent":null,"ImpressionLostToRankAggPercent":null,"QualityScore":5,"ExpectedCtr":2.0,"AdRelevance":3,"LandingPageExperience":1,"HistoricalQualityScore":null,"HistoricalExpectedCtr":null,"HistoricalAdRelevance":null,"HistoricalLandingPageExperience":null,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","CampaignStatus":"Active","AdGroupLabels":null,"ExactMatchImpressionSharePercent":null,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":null,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":null,"TopImpressionShareLostToBudgetPercent":null,"AbsoluteTopImpressionShareLostToRankPercent":null,"AbsoluteTopImpressionShareLostToBudgetPercent":null,"TopImpressionSharePercent":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AdGroupType":"Standard","AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":null,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1699955374204} {"stream":"ad_group_impression_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-05","Status":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":3,"Clicks":1,"Ctr":33.33,"AverageCpc":0.08,"Spend":0.08,"AveragePosition":0.0,"Conversions":0,"ConversionRate":0.0,"CostPerConversion":null,"DeviceType":"Computer","Language":"Danish","ImpressionSharePercent":11.11,"ImpressionLostToBudgetPercent":0.0,"ImpressionLostToRankAggPercent":88.89,"QualityScore":5,"ExpectedCtr":2.0,"AdRelevance":3,"LandingPageExperience":1,"HistoricalQualityScore":5,"HistoricalExpectedCtr":2,"HistoricalAdRelevance":3,"HistoricalLandingPageExperience":1,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Microsoft sites and select traffic","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","CampaignStatus":"Active","AdGroupLabels":null,"ExactMatchImpressionSharePercent":null,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":null,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":null,"TopImpressionShareLostToBudgetPercent":null,"AbsoluteTopImpressionShareLostToRankPercent":null,"AbsoluteTopImpressionShareLostToBudgetPercent":null,"TopImpressionSharePercent":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":0.0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AdGroupType":"Standard","AverageCpm":26.67,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":null,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1699955407065} -{"stream":"ad_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"AdId":84800390693061,"TimePeriod":"2023-11-07","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Language":"German","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","DeliveredMatchType":"Phrase","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","Impressions":2,"Clicks":1,"Ctr":50.0,"Spend":0.79,"CostPerConversion":null,"DestinationUrl":null,"Assists":0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"AdDescription":null,"AdDescription2":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"Conversions":0.0,"ConversionRate":0.0,"ConversionsQualified":0.0,"AverageCpc":0.79,"AveragePosition":0.0,"AverageCpm":395.0,"AllConversions":0,"AllConversionRate":0.0,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1699953597299} -{"stream":"ad_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"AdId":84800390693061,"TimePeriod":"2023-11-05","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Language":"Danish","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","DeliveredMatchType":"Phrase","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","Impressions":3,"Clicks":1,"Ctr":33.33,"Spend":0.08,"CostPerConversion":null,"DestinationUrl":null,"Assists":0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"AdDescription":null,"AdDescription2":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"Conversions":0.0,"ConversionRate":0.0,"ConversionsQualified":0.0,"AverageCpc":0.08,"AveragePosition":0.0,"AverageCpm":26.67,"AllConversions":0,"AllConversionRate":0.0,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1699953623795} -{"stream":"budget_summary_report","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"CampaignName":"Airbyte test","CampaignId":531016227,"Date":"11/7/2023","MonthlyBudget":48.0,"DailySpend":3.12,"MonthToDateSpend":3.12},"emitted_at":1699953978176} +{"stream":"ad_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"AdId":84800390693061,"TimePeriod":"2023-11-07","AbsoluteTopImpressionRatePercent":0.0,"TopImpressionRatePercent":100.0,"CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Language":"German","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","DeliveredMatchType":"Phrase","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","Impressions":2,"Clicks":1,"Ctr":50.0,"Spend":0.79,"CostPerConversion":null,"DestinationUrl":null,"Assists":0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"AdDescription":null,"AdDescription2":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"Conversions":0.0,"ConversionRate":0.0,"ConversionsQualified":0.0,"AverageCpc":0.79,"AveragePosition":0.0,"AverageCpm":395.0,"AllConversions":0,"AllConversionRate":0.0,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1700076965521} +{"stream":"ad_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"AdId":84800390693061,"TimePeriod":"2023-11-05","AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Language":"Danish","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","DeliveredMatchType":"Phrase","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","AdGroupName":"keywords","Impressions":3,"Clicks":1,"Ctr":33.33,"Spend":0.08,"CostPerConversion":null,"DestinationUrl":null,"Assists":0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"AdDescription":null,"AdDescription2":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"Conversions":0.0,"ConversionRate":0.0,"ConversionsQualified":0.0,"AverageCpc":0.08,"AveragePosition":0.0,"AverageCpm":26.67,"AllConversions":0,"AllConversionRate":0.0,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1700077221065} +{"stream":"budget_summary_report","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"CampaignName":"Airbyte test","CampaignId":531016227,"Date":"2023-11-07","MonthlyBudget":48.0,"DailySpend":3.12,"MonthToDateSpend":3.12},"emitted_at":1699953978176} {"stream":"campaign_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"TimePeriod":"2023-11-07","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Phrase","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Other","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","CampaignStatus":"Active","CampaignLabels":null,"Impressions":0,"Clicks":0,"Ctr":null,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"AdRelevance":3.0,"LandingPageExperience":1.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"LowQualityClicks":0,"LowQualityClicksPercent":null,"LowQualityImpressions":1,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null,"BudgetName":null,"BudgetStatus":null,"BudgetAssociationStatus":"Current","HistoricalQualityScore":null,"HistoricalExpectedCtr":null,"HistoricalAdRelevance":null,"HistoricalLandingPageExperience":null},"emitted_at":1699954051067} {"stream":"campaign_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"TimePeriod":"2023-11-05","CurrencyCode":"USD","AdDistribution":"Search","DeviceType":"Computer","Network":"Syndicated search partners","DeliveredMatchType":"Exact","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","CampaignType":"Search & content","CampaignStatus":"Active","CampaignLabels":null,"Impressions":9,"Clicks":1,"Ctr":11.11,"Spend":0.03,"CostPerConversion":null,"QualityScore":5.0,"AdRelevance":3.0,"LandingPageExperience":1.0,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Assists":0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"CustomParameters":null,"ViewThroughConversions":0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"AverageCpc":0.03,"AveragePosition":0.0,"AverageCpm":3.33,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"LowQualityClicks":0,"LowQualityClicksPercent":0.0,"LowQualityImpressions":0,"LowQualitySophisticatedClicks":0,"LowQualityConversions":0,"LowQualityConversionRate":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null,"BudgetName":null,"BudgetStatus":null,"BudgetAssociationStatus":"Current","HistoricalQualityScore":5.0,"HistoricalExpectedCtr":2.0,"HistoricalAdRelevance":3.0,"HistoricalLandingPageExperience":1.0},"emitted_at":1699954081143} {"stream":"campaign_impression_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-07","CampaignStatus":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":10,"Clicks":1,"Ctr":10.0,"AverageCpc":0.33,"Spend":0.33,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"LowQualityClicks":0,"LowQualityClicksPercent":0.0,"LowQualityImpressions":9,"LowQualityImpressionsPercent":47.37,"LowQualityConversions":0,"LowQualityConversionRate":null,"DeviceType":"Computer","ImpressionSharePercent":3.37,"ImpressionLostToBudgetPercent":85.19,"ImpressionLostToRankAggPercent":11.45,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"HistoricalQualityScore":null,"HistoricalExpectedCtr":null,"HistoricalAdRelevance":null,"HistoricalLandingPageExperience":null,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":0,"CampaignLabels":null,"ExactMatchImpressionSharePercent":null,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":6.02,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":14.63,"TopImpressionShareLostToBudgetPercent":77.24,"AbsoluteTopImpressionShareLostToRankPercent":15.66,"AbsoluteTopImpressionShareLostToBudgetPercent":78.31,"TopImpressionSharePercent":8.13,"AbsoluteTopImpressionRatePercent":50.0,"TopImpressionRatePercent":100.0,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AverageCpm":33.0,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":null,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1699954182626} {"stream":"campaign_impression_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-05","CampaignStatus":"Active","CampaignName":"Airbyte test","CampaignId":531016227,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":10,"Clicks":1,"Ctr":10.0,"AverageCpc":0.33,"Spend":0.33,"AveragePosition":0.0,"Conversions":0,"ConversionRate":null,"CostPerConversion":null,"LowQualityClicks":0,"LowQualityClicksPercent":0.0,"LowQualityImpressions":9,"LowQualityImpressionsPercent":47.37,"LowQualityConversions":0,"LowQualityConversionRate":null,"DeviceType":"Computer","ImpressionSharePercent":10.87,"ImpressionLostToBudgetPercent":17.05,"ImpressionLostToRankAggPercent":72.08,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"HistoricalQualityScore":null,"HistoricalExpectedCtr":null,"HistoricalAdRelevance":null,"HistoricalLandingPageExperience":null,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"TrackingTemplate":null,"CustomParameters":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":0,"CampaignLabels":null,"ExactMatchImpressionSharePercent":29.07,"ClickSharePercent":2.89,"AbsoluteTopImpressionSharePercent":8.88,"FinalUrlSuffix":null,"CampaignType":"Search & content","TopImpressionShareLostToRankPercent":76.51,"TopImpressionShareLostToBudgetPercent":9.99,"AbsoluteTopImpressionShareLostToRankPercent":81.99,"AbsoluteTopImpressionShareLostToBudgetPercent":9.13,"TopImpressionSharePercent":13.5,"AbsoluteTopImpressionRatePercent":50.0,"TopImpressionRatePercent":100.0,"BaseCampaignId":531016227,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"RelativeCtr":null,"AverageCpm":33.0,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":null,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1699954211223} -{"stream":"keyword_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"KeywordId":84801135055365,"AdId":84800390693061,"TimePeriod":"2023-11-07","CurrencyCode":"USD","DeliveredMatchType":"Phrase","AdDistribution":"Search","DeviceType":"Computer","Language":"German","Network":"Syndicated search partners","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","Keyword":"connector","KeywordStatus":"Active","Impressions":1,"Clicks":0,"Ctr":0.0,"CurrentMaxCpc":2.27,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"QualityImpact":0.0,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"Mainline1Bid":null,"MainlineBid":1.2,"FirstPageBid":0.56,"FinalUrlSuffix":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null,"HistoricalQualityScore":null,"HistoricalExpectedCtr":null,"HistoricalAdRelevance":null,"HistoricalLandingPageExperience":null},"emitted_at":1699954312976} -{"stream":"keyword_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"KeywordId":84801135055365,"AdId":84800390693061,"TimePeriod":"2023-11-05","CurrencyCode":"USD","DeliveredMatchType":"Exact","AdDistribution":"Search","DeviceType":"Computer","Language":"English","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","Keyword":"connector","KeywordStatus":"Active","Impressions":2,"Clicks":0,"Ctr":0.0,"CurrentMaxCpc":2.27,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"QualityImpact":0.0,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"Mainline1Bid":null,"MainlineBid":1.2,"FirstPageBid":0.56,"FinalUrlSuffix":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1699954348243} +{"stream":"keyword_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"KeywordId":84801135055365,"Keyword":"connector","AdId":84800390693061,"TimePeriod":"2023-11-07","CurrencyCode":"USD","DeliveredMatchType":"Phrase","AdDistribution":"Search","DeviceType":"Computer","Language":"German","Network":"Syndicated search partners","DeviceOS":"Unknown","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","KeywordStatus":"Active","HistoricalExpectedCtr":null,"HistoricalAdRelevance":null,"HistoricalLandingPageExperience":null,"HistoricalQualityScore":null,"Impressions":1,"Clicks":0,"Ctr":0.0,"CurrentMaxCpc":2.27,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"QualityImpact":0.0,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"Mainline1Bid":null,"MainlineBid":1.18,"FirstPageBid":0.51,"FinalUrlSuffix":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1700078149400} +{"stream":"keyword_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"KeywordId":84801135055365,"Keyword":"connector","AdId":84800390693061,"TimePeriod":"2023-11-05","CurrencyCode":"USD","DeliveredMatchType":"Exact","AdDistribution":"Search","DeviceType":"Computer","Language":"English","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","AccountName":"Airbyte","CampaignName":"Airbyte test","AdGroupName":"keywords","KeywordStatus":"Active","Impressions":2,"Clicks":0,"Ctr":0.0,"CurrentMaxCpc":2.27,"Spend":0.0,"CostPerConversion":null,"QualityScore":5.0,"ExpectedCtr":"2","AdRelevance":3.0,"LandingPageExperience":1.0,"QualityImpact":0.0,"Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"CustomParameters":null,"FinalAppUrl":null,"Mainline1Bid":null,"MainlineBid":1.18,"FirstPageBid":0.51,"FinalUrlSuffix":null,"ViewThroughConversions":0,"ViewThroughConversionsQualified":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1700078299436} {"stream":"geographic_performance_report_daily","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-11-07","Country":"Australia","CurrencyCode":"USD","DeliveredMatchType":"Broad","AdDistribution":"Search","DeviceType":"Computer","Language":"English","Network":"Syndicated search partners","DeviceOS":"Windows","TopVsOther":"Syndicated search partners - Top","BidMatchType":"Broad","MetroArea":null,"State":"New South Wales","City":null,"AdGroupName":"keywords","Ctr":0.0,"ProximityTargetLocation":null,"Radius":"0","Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"LocationType":"Physical location","MostSpecificLocation":"2000","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","County":null,"PostalCode":"2000","LocationId":"122395","BaseCampaignId":"531016227","AllCostPerConversion":null,"AllReturnOnAdSpend":null,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":0.0,"TopImpressionRatePercent":"100.00","AllConversionsQualified":"0.00","ViewThroughConversionsQualified":null,"Neighborhood":null,"ViewThroughRevenue":"0.00","CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null,"AssetGroupStatus":null,"Clicks":0,"Spend":0.0,"Impressions":1,"CostPerConversion":null,"AccountName":"Airbyte","AccountNumber":"F149MJ18","CampaignName":"Airbyte test","Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1699956863587} {"stream":"geographic_performance_report_weekly","data":{"AccountId":180519267,"CampaignId":531016227,"AdGroupId":1356799861840328,"TimePeriod":"2023-11-05","Country":"Argentina","CurrencyCode":"USD","DeliveredMatchType":"Exact","AdDistribution":"Search","DeviceType":"Computer","Language":"Spanish","Network":"Microsoft sites and select traffic","DeviceOS":"Windows","TopVsOther":"Microsoft sites and select traffic - top","BidMatchType":"Broad","MetroArea":null,"State":"Buenos Aires Province","City":null,"AdGroupName":"keywords","Ctr":0.0,"ProximityTargetLocation":null,"Radius":"0","Assists":0,"ReturnOnAdSpend":null,"CostPerAssist":null,"LocationType":"Physical location","MostSpecificLocation":"Buenos Aires Province","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","County":null,"PostalCode":null,"LocationId":"141965","BaseCampaignId":"531016227","AllCostPerConversion":null,"AllReturnOnAdSpend":null,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":0.0,"TopImpressionRatePercent":"100.00","AllConversionsQualified":"0.00","ViewThroughConversionsQualified":null,"Neighborhood":null,"ViewThroughRevenue":"0.00","CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null,"AssetGroupStatus":null,"Clicks":0,"Spend":0.0,"Impressions":1,"CostPerConversion":null,"AccountName":"Airbyte","AccountNumber":"F149MJ18","CampaignName":"Airbyte test","Conversions":0.0,"ConversionRate":null,"ConversionsQualified":0.0,"AverageCpc":0.0,"AveragePosition":0.0,"AverageCpm":0.0,"AllConversions":0,"AllConversionRate":null,"AllRevenue":0.0,"AllRevenuePerConversion":null,"Revenue":0.0,"RevenuePerConversion":null,"RevenuePerAssist":null},"emitted_at":1699953673210} {"stream":"age_gender_audience_report_daily","data":{"AccountId":180519267,"AgeGroup":"Unknown","Gender":"Unknown","TimePeriod":"2023-11-07","AllConversions":0,"AccountName":"Airbyte","AccountNumber":"F149MJ18","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"AdDistribution":"Search","Impressions":3,"Clicks":1,"Conversions":0.0,"Spend":0.79,"Revenue":0.0,"ExtendedCost":0.0,"Assists":0,"Language":"German","AccountStatus":"Active","CampaignStatus":"Active","AdGroupStatus":"Active","BaseCampaignId":"531016227","AllRevenue":0.0,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":33.33,"TopImpressionRatePercent":100.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0},"emitted_at":1699954406862} @@ -27,10 +27,10 @@ {"stream":"user_location_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-05","CampaignName":"Airbyte test","CampaignId":531016227,"AdGroupName":"keywords","AdGroupId":1356799861840328,"Country":"Argentina","State":null,"MetroArea":null,"CurrencyCode":"USD","AdDistribution":"Search","Impressions":1,"Clicks":0,"Ctr":0.0,"AverageCpc":0.0,"Spend":0.0,"AveragePosition":0.0,"ProximityTargetLocation":null,"Radius":0,"Language":"Spanish","City":null,"QueryIntentCountry":"Argentina","QueryIntentState":null,"QueryIntentCity":null,"QueryIntentDMA":null,"BidMatchType":"Broad","DeliveredMatchType":"Phrase","Network":"Syndicated search partners","TopVsOther":"Syndicated search partners - Top","DeviceType":"Computer","DeviceOS":"Unknown","Assists":0,"Conversions":0,"ConversionRate":null,"Revenue":0.0,"ReturnOnAdSpend":null,"CostPerConversion":null,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"County":null,"PostalCode":null,"QueryIntentCounty":null,"QueryIntentPostalCode":null,"LocationId":8,"QueryIntentLocationId":8,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":null,"AllCostPerConversion":null,"AllReturnOnAdSpend":null,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"Goal":null,"GoalType":null,"AbsoluteTopImpressionRatePercent":100.0,"TopImpressionRatePercent":100.0,"AverageCpm":0.0,"ConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"Neighborhood":null,"QueryIntentNeighborhood":null,"ViewThroughRevenue":0.0,"CampaignType":"Search & content","AssetGroupId":null,"AssetGroupName":null},"emitted_at":1699954842196} {"stream":"account_impression_performance_report_daily","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-07","CurrencyCode":"USD","AdDistribution":"Search","Impressions":10,"Clicks":1,"Ctr":10.0,"AverageCpc":0.33,"Spend":0.33,"AveragePosition":0.0,"Conversions":0,"ConversionRate":0.0,"CostPerConversion":null,"LowQualityClicks":0,"LowQualityClicksPercent":0.0,"LowQualityImpressions":9,"LowQualityImpressionsPercent":47.37,"LowQualityConversions":0,"LowQualityConversionRate":null,"DeviceType":"Computer","ImpressionSharePercent":3.37,"ImpressionLostToBudgetPercent":85.19,"ImpressionLostToRankAggPercent":11.45,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"AccountStatus":"Active","LowQualityGeneralClicks":0,"LowQualitySophisticatedClicks":0,"ExactMatchImpressionSharePercent":null,"ClickSharePercent":null,"AbsoluteTopImpressionSharePercent":6.02,"TopImpressionShareLostToRankPercent":14.63,"TopImpressionShareLostToBudgetPercent":77.24,"AbsoluteTopImpressionShareLostToRankPercent":15.66,"AbsoluteTopImpressionShareLostToBudgetPercent":78.31,"TopImpressionSharePercent":8.13,"AbsoluteTopImpressionRatePercent":50.0,"TopImpressionRatePercent":100.0,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":0.0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"AverageCpm":33.0,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":null,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1699955144374} {"stream":"account_impression_performance_report_weekly","data":{"AccountName":"Airbyte","AccountNumber":"F149MJ18","AccountId":180519267,"TimePeriod":"2023-11-05","CurrencyCode":"USD","AdDistribution":"Search","Impressions":457,"Clicks":5,"Ctr":1.09,"AverageCpc":0.38,"Spend":1.88,"AveragePosition":0.0,"Conversions":0,"ConversionRate":0.0,"CostPerConversion":null,"LowQualityClicks":6,"LowQualityClicksPercent":54.55,"LowQualityImpressions":183,"LowQualityImpressionsPercent":28.59,"LowQualityConversions":0,"LowQualityConversionRate":0.0,"DeviceType":"Computer","ImpressionSharePercent":10.87,"ImpressionLostToBudgetPercent":17.05,"ImpressionLostToRankAggPercent":72.08,"PhoneImpressions":0,"PhoneCalls":0,"Ptr":null,"Network":"Syndicated search partners","Assists":0,"Revenue":0.0,"ReturnOnAdSpend":0.0,"CostPerAssist":null,"RevenuePerConversion":null,"RevenuePerAssist":null,"AccountStatus":"Active","LowQualityGeneralClicks":4,"LowQualitySophisticatedClicks":2,"ExactMatchImpressionSharePercent":29.07,"ClickSharePercent":2.89,"AbsoluteTopImpressionSharePercent":8.88,"TopImpressionShareLostToRankPercent":76.51,"TopImpressionShareLostToBudgetPercent":9.99,"AbsoluteTopImpressionShareLostToRankPercent":81.99,"AbsoluteTopImpressionShareLostToBudgetPercent":9.13,"TopImpressionSharePercent":13.5,"AbsoluteTopImpressionRatePercent":39.61,"TopImpressionRatePercent":81.62,"AllConversions":0,"AllRevenue":0.0,"AllConversionRate":0.0,"AllCostPerConversion":null,"AllReturnOnAdSpend":0.0,"AllRevenuePerConversion":null,"ViewThroughConversions":0,"AudienceImpressionSharePercent":null,"AudienceImpressionLostToRankPercent":null,"AudienceImpressionLostToBudgetPercent":null,"AverageCpm":4.11,"ConversionsQualified":0.0,"LowQualityConversionsQualified":0.0,"AllConversionsQualified":0.0,"ViewThroughConversionsQualified":null,"ViewThroughRevenue":0.0,"VideoViews":0,"ViewThroughRate":0.0,"AverageCPV":null,"VideoViewsAt25Percent":0,"VideoViewsAt50Percent":0,"VideoViewsAt75Percent":0,"CompletedVideoViews":0,"VideoCompletionRate":null,"TotalWatchTimeInMS":0,"AverageWatchTimePerVideoView":null,"AverageWatchTimePerImpression":0.0,"Sales":0,"CostPerSale":null,"RevenuePerSale":null,"Installs":0,"CostPerInstall":null,"RevenuePerInstall":null},"emitted_at":1699955205307} -{"stream":"labels","data":{"Status":"Active","Id":10239203506495,"Client Id":null,"Modified Time":"04/27/2023 17:16:49.170","Description":null,"Label":"what a new label","Color":"#B0B20F","Account Id":180278106},"emitted_at":1698687185070} -{"stream":"campaign_labels","data":{"Status":"Active","Id":10239203506494,"Parent Id":413444833,"Campaign":null,"Client Id":null,"Modified Time":"04/27/2023 17:57:38.110","Account Id":180278106},"emitted_at":1698687214960} -{"stream":"keyword_labels","data":{"Status":"Active","Id":10239203506495,"Parent Id":84868925026027,"Client Id":null,"Modified Time":"04/27/2023 17:22:52.733","Account Id":180278106},"emitted_at":1698687224964} -{"stream":"ad_group_labels","data":{"Status":"Active","Id":10239203506494,"Parent Id":1350201453189474,"Campaign":null,"Ad Group":null,"Client Id":null,"Modified Time":"04/27/2023 18:00:14.970","Account Id":180278106},"emitted_at":1698751388918} -{"stream": "keywords", "data": {"Status": "Active", "Id": 84525593559627, "Parent Id": "1352400325389092", "Campaign": "Test 2", "Ad Group": "Airbyte", "Client Id": null, "Modified Time": "08/13/2021 02:33:08.560", "Tracking Template": null, "Final Url Suffix": null, "Custom Parameter": null, "Final Url": null, "Mobile Final Url": null, "Bid Strategy Type": "InheritFromParent", "Inherited Bid Strategy Type": "MaxClicks", "Destination Url": null, "Editorial Status": "Active", "Editorial Location": null, "Editorial Term": null, "Editorial Reason Code": null, "Editorial Appeal Status": null, "Keyword": "big data integration tools", "Match Type": "Broad", "Bid": "0.11", "Param1": null, "Param2": null, "Param3": null, "Publisher Countries": null, "Quality Score": null, "Keyword Relevance": null, "Landing Page Relevance": null, "Landing Page User Experience": null, "Account Id": 180278106}, "emitted_at": 1698768109942} -{"stream": "keywords", "data": {"Status": "Active", "Id": 84525593559628, "Parent Id": "1352400325389092", "Campaign": "Test 2", "Ad Group": "Airbyte", "Client Id": null, "Modified Time": "08/13/2021 02:33:14.270", "Tracking Template": null, "Final Url Suffix": null, "Custom Parameter": null, "Final Url": null, "Mobile Final Url": null, "Bid Strategy Type": "InheritFromParent", "Inherited Bid Strategy Type": "MaxClicks", "Destination Url": null, "Editorial Status": "Active", "Editorial Location": null, "Editorial Term": null, "Editorial Reason Code": null, "Editorial Appeal Status": null, "Keyword": "best data integration tool", "Match Type": "Broad", "Bid": "0.11", "Param1": null, "Param2": null, "Param3": null, "Publisher Countries": null, "Quality Score": null, "Keyword Relevance": null, "Landing Page Relevance": null, "Landing Page User Experience": null, "Account Id": 180278106}, "emitted_at": 1698768109942} -{"stream": "keywords", "data": {"Status": "Active", "Id": 84525593559629, "Parent Id": "1352400325389092", "Campaign": "Test 2", "Ad Group": "Airbyte", "Client Id": null, "Modified Time": "08/13/2021 02:33:19.053", "Tracking Template": null, "Final Url Suffix": null, "Custom Parameter": null, "Final Url": null, "Mobile Final Url": null, "Bid Strategy Type": "InheritFromParent", "Inherited Bid Strategy Type": "MaxClicks", "Destination Url": null, "Editorial Status": "Active", "Editorial Location": null, "Editorial Term": null, "Editorial Reason Code": null, "Editorial Appeal Status": null, "Keyword": "data integration tools", "Match Type": "Broad", "Bid": "0.11", "Param1": null, "Param2": null, "Param3": null, "Publisher Countries": null, "Quality Score": null, "Keyword Relevance": null, "Landing Page Relevance": null, "Landing Page User Experience": null, "Account Id": 180278106}, "emitted_at": 1698768109943} +{"stream":"labels","data":{"Status":"Active","Id":10239203506495,"Client Id":null,"Modified Time":"2023-04-27T17:16:49.170+00:00","Description":null,"Label":"what a new label","Color":"#B0B20F","Account Id":180278106},"emitted_at":1698687185070} +{"stream":"campaign_labels","data":{"Status":"Active","Id":10239203506494,"Parent Id":413444833,"Campaign":null,"Client Id":null,"Modified Time":"2023-04-27T17:57:38.110+00:00","Account Id":180278106},"emitted_at":1698687214960} +{"stream":"keyword_labels","data":{"Status":"Active","Id":10239203506495,"Parent Id":84868925026027,"Client Id":null,"Modified Time":"2023-04-27T17:22:52.733+00:00","Account Id":180278106},"emitted_at":1698687224964} +{"stream":"ad_group_labels","data":{"Status":"Active","Id":10239203506494,"Parent Id":1350201453189474,"Campaign":null,"Ad Group":null,"Client Id":null,"Modified Time":"2023-04-27T18:00:14.970+00:00","Account Id":180278106},"emitted_at":1698751388918} +{"stream": "keywords", "data": {"Status": "Active", "Id": 84525593559627, "Parent Id": "1352400325389092", "Campaign": "Test 2", "Ad Group": "Airbyte", "Client Id": null, "Modified Time": "2021-08-13T02:33:08.560+00:00", "Tracking Template": null, "Final Url Suffix": null, "Custom Parameter": null, "Final Url": null, "Mobile Final Url": null, "Bid Strategy Type": "InheritFromParent", "Inherited Bid Strategy Type": "MaxClicks", "Destination Url": null, "Editorial Status": "Active", "Editorial Location": null, "Editorial Term": null, "Editorial Reason Code": null, "Editorial Appeal Status": null, "Keyword": "big data integration tools", "Match Type": "Broad", "Bid": "0.11", "Param1": null, "Param2": null, "Param3": null, "Publisher Countries": null, "Quality Score": null, "Keyword Relevance": null, "Landing Page Relevance": null, "Landing Page User Experience": null, "Account Id": 180278106}, "emitted_at": 1698768109942} +{"stream": "keywords", "data": {"Status": "Active", "Id": 84525593559628, "Parent Id": "1352400325389092", "Campaign": "Test 2", "Ad Group": "Airbyte", "Client Id": null, "Modified Time": "2021-08-13T02:33:14.270+00:00", "Tracking Template": null, "Final Url Suffix": null, "Custom Parameter": null, "Final Url": null, "Mobile Final Url": null, "Bid Strategy Type": "InheritFromParent", "Inherited Bid Strategy Type": "MaxClicks", "Destination Url": null, "Editorial Status": "Active", "Editorial Location": null, "Editorial Term": null, "Editorial Reason Code": null, "Editorial Appeal Status": null, "Keyword": "best data integration tool", "Match Type": "Broad", "Bid": "0.11", "Param1": null, "Param2": null, "Param3": null, "Publisher Countries": null, "Quality Score": null, "Keyword Relevance": null, "Landing Page Relevance": null, "Landing Page User Experience": null, "Account Id": 180278106}, "emitted_at": 1698768109942} +{"stream": "keywords", "data": {"Status": "Active", "Id": 84525593559629, "Parent Id": "1352400325389092", "Campaign": "Test 2", "Ad Group": "Airbyte", "Client Id": null, "Modified Time": "2021-08-13T02:33:19.053+00:00", "Tracking Template": null, "Final Url Suffix": null, "Custom Parameter": null, "Final Url": null, "Mobile Final Url": null, "Bid Strategy Type": "InheritFromParent", "Inherited Bid Strategy Type": "MaxClicks", "Destination Url": null, "Editorial Status": "Active", "Editorial Location": null, "Editorial Term": null, "Editorial Reason Code": null, "Editorial Appeal Status": null, "Keyword": "data integration tools", "Match Type": "Broad", "Bid": "0.11", "Param1": null, "Param2": null, "Param3": null, "Publisher Countries": null, "Quality Score": null, "Keyword Relevance": null, "Landing Page Relevance": null, "Landing Page User Experience": null, "Account Id": 180278106}, "emitted_at": 1698768109943} diff --git a/airbyte-integrations/connectors/source-bing-ads/integration_tests/state.json b/airbyte-integrations/connectors/source-bing-ads/integration_tests/sample_state.json similarity index 61% rename from airbyte-integrations/connectors/source-bing-ads/integration_tests/state.json rename to airbyte-integrations/connectors/source-bing-ads/integration_tests/sample_state.json index ef99548a8bb29..8b6447f78e3ea 100644 --- a/airbyte-integrations/connectors/source-bing-ads/integration_tests/state.json +++ b/airbyte-integrations/connectors/source-bing-ads/integration_tests/sample_state.json @@ -4,7 +4,7 @@ "stream": { "stream_state": { "180278106": { - "TimePeriod": 1627820152 + "TimePeriod": "2021-08-01T14:00:00+00:00" } }, "stream_descriptor": { "name": "keyword_performance_report_hourly" } @@ -15,10 +15,10 @@ "stream": { "stream_state": { "180278106": { - "Date": 1627800152 + "Date": "2021-08-01" } }, - "stream_descriptor": { "name": "budget_summary_report_hourly" } + "stream_descriptor": { "name": "budget_summary_report" } } }, { @@ -26,10 +26,10 @@ "stream": { "stream_state": { "180278106": { - "TimePeriod": 1627795152 + "TimePeriod": "2021-08-01" } }, - "stream_descriptor": { "name": "ad_performance_report_hourly" } + "stream_descriptor": { "name": "ad_performance_report_weekly" } } }, { @@ -37,10 +37,10 @@ "stream": { "stream_state": { "180278106": { - "TimePeriod": 1727810152 + "TimePeriod": "2023-04-27T15:16:49.170+00:00" } }, - "stream_descriptor": { "name": "campaign_performance_report_hourly" } + "stream_descriptor": { "name": "labels" } } } ] diff --git a/airbyte-integrations/connectors/source-bing-ads/metadata.yaml b/airbyte-integrations/connectors/source-bing-ads/metadata.yaml index 2fa6e2d659d92..63f2a336f97f4 100644 --- a/airbyte-integrations/connectors/source-bing-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-bing-ads/metadata.yaml @@ -16,7 +16,7 @@ data: connectorSubtype: api connectorType: source definitionId: 47f25999-dd5e-4636-8c39-e7cea2453331 - dockerImageTag: 1.13.0 + dockerImageTag: 2.0.0 dockerRepository: airbyte/source-bing-ads documentationUrl: https://docs.airbyte.com/integrations/sources/bing-ads githubIssueLabel: source-bing-ads @@ -39,6 +39,11 @@ data: primary keys. A data reset and schema refresh of all the affected streams is required for the changes to take effect. upgradeDeadline: "2023-10-25" + 2.0.0: + message: > + Version 2.0.0 updates schemas for all hourly reports (end in report_hourly), and the following streams: Accounts, Campaigns, Search Query Performance Report, AppInstallAds, AppInstallAdLabels, Labels, Campaign Labels, Keyword Labels, Ad Group Labels, Keywords, and Budget Summary Report. + Users will need to refresh the source schema and reset affected streams after upgrading. + upgradeDeadline: "2023-11-29" suggestedStreams: streams: - campaigns diff --git a/airbyte-integrations/connectors/source-bing-ads/setup.py b/airbyte-integrations/connectors/source-bing-ads/setup.py index 29de9b3aded40..5c326b2d9aad6 100644 --- a/airbyte-integrations/connectors/source-bing-ads/setup.py +++ b/airbyte-integrations/connectors/source-bing-ads/setup.py @@ -8,6 +8,7 @@ MAIN_REQUIREMENTS = ["airbyte-cdk", "bingads~=13.0.17", "urllib3<2.0", "pandas"] TEST_REQUIREMENTS = [ + "freezegun", "requests-mock~=1.9.3", "pytest-mock~=3.6.1", "pytest~=6.1", diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/base_streams.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/base_streams.py new file mode 100644 index 0000000000000..39f1eec1957e2 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/base_streams.py @@ -0,0 +1,326 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# +import ssl +import time +from abc import ABC, abstractmethod +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Union +from urllib.error import URLError + +from airbyte_cdk.models import SyncMode +from airbyte_cdk.sources.streams import Stream +from bingads.service_client import ServiceClient +from bingads.v13.reporting.reporting_service_manager import ReportingServiceManager +from source_bing_ads.client import Client +from suds import sudsobject + + +class BingAdsBaseStream(Stream, ABC): + primary_key: Optional[Union[str, List[str], List[List[str]]]] = None + + def __init__(self, client: Client, config: Mapping[str, Any]) -> None: + super().__init__() + self.client = client + self.config = config + + +class BingAdsStream(BingAdsBaseStream, ABC): + @property + @abstractmethod + def operation_name(self) -> str: + """ + Specifies operation name to use for a current stream + """ + + @property + @abstractmethod + def service_name(self) -> str: + """ + Specifies bing ads service name for a current stream + """ + + @property + def parent_key_to_foreign_key_map(self) -> MutableMapping[str, str]: + """ + Specifies dict with field in record as kay and slice key as value to be inserted in record in transform method. + """ + return {} + + def transform(self, record: MutableMapping[str, Any], stream_slice: Mapping[str, Any], **kwargs) -> MutableMapping[str, Any]: + foreign_keys = {key: stream_slice.get(value) for key, value in self.parent_key_to_foreign_key_map.items()} + return record | foreign_keys + + @property + def _service(self) -> Union[ServiceClient, ReportingServiceManager]: + return self.client.get_service(service_name=self.service_name) + + @property + def _user_id(self) -> int: + return self._get_user_id() + + # TODO remove once Microsoft support confirm their SSL certificates are always valid... + def _get_user_id(self, number_of_retries=10): + """""" + try: + return self._service.GetUser().User.Id + except URLError as error: + if isinstance(error.reason, ssl.SSLError): + self.logger.warning("SSL certificate error, retrying...") + if number_of_retries > 0: + time.sleep(1) + return self._get_user_id(number_of_retries - 1) + else: + raise error + + def next_page_token(self, response: sudsobject.Object, **kwargs: Mapping[str, Any]) -> Optional[Mapping[str, Any]]: + """ + Default method for streams that don't support pagination + """ + return None + + def send_request(self, params: Mapping[str, Any], customer_id: str, account_id: str = None) -> Mapping[str, Any]: + request_kwargs = { + "service_name": self.service_name, + "customer_id": customer_id, + "account_id": account_id, + "operation_name": self.operation_name, + "params": params, + } + request = self.client.request(**request_kwargs) + return request + + def read_records( + self, + sync_mode: SyncMode, + stream_slice: Mapping[str, Any] = None, + stream_state: Mapping[str, Any] = None, + **kwargs: Mapping[str, Any], + ) -> Iterable[Mapping[str, Any]]: + stream_state = stream_state or {} + next_page_token = None + account_id = str(stream_slice.get("account_id")) if stream_slice else None + customer_id = str(stream_slice.get("customer_id")) if stream_slice else None + + while True: + params = self.request_params( + stream_state=stream_state, + stream_slice=stream_slice, + next_page_token=next_page_token, + account_id=account_id, + ) + response = self.send_request(params, customer_id=customer_id, account_id=account_id) + for record in self.parse_response(response): + yield self.transform(record, stream_slice) + + next_page_token = self.next_page_token(response, current_page_token=next_page_token) + if not next_page_token: + break + + def parse_response(self, response: sudsobject.Object, **kwargs) -> Iterable[Mapping]: + if response is not None and hasattr(response, self.data_field): + yield from self.client.asdict(response)[self.data_field] + + +class BingAdsCampaignManagementStream(BingAdsStream, ABC): + service_name: str = "CampaignManagement" + + @property + @abstractmethod + def data_field(self) -> str: + """ + Specifies root object name in a stream response + """ + + @property + @abstractmethod + def additional_fields(self) -> Optional[str]: + """ + Specifies which additional fields to fetch for a current stream. + Expected format: field names separated by space + """ + + def parse_response(self, response: sudsobject.Object, **kwargs) -> Iterable[Mapping]: + if response is not None and hasattr(response, self.data_field): + yield from self.client.asdict(response)[self.data_field] + + +class Accounts(BingAdsStream): + """ + Searches for accounts that the current authenticated user can access. + API doc: https://docs.microsoft.com/en-us/advertising/customer-management-service/searchaccounts?view=bingads-13 + Account schema: https://docs.microsoft.com/en-us/advertising/customer-management-service/advertiseraccount?view=bingads-13 + Stream caches incoming responses to be able to reuse this data in Campaigns stream + """ + + primary_key = "Id" + # Stream caches incoming responses to avoid duplicated http requests + use_cache: bool = True + data_field: str = "AdvertiserAccount" + service_name: str = "CustomerManagementService" + operation_name: str = "SearchAccounts" + additional_fields: str = "TaxCertificate AccountMode" + # maximum page size + page_size_limit: int = 1000 + + def next_page_token(self, response: sudsobject.Object, current_page_token: Optional[int]) -> Optional[Mapping[str, Any]]: + current_page_token = current_page_token or 0 + if response is not None and hasattr(response, self.data_field): + return None if self.page_size_limit > len(response[self.data_field]) else current_page_token + 1 + else: + return None + + def request_params( + self, + next_page_token: Mapping[str, Any] = None, + **kwargs: Mapping[str, Any], + ) -> MutableMapping[str, Any]: + predicates = { + "Predicate": [ + { + "Field": "UserId", + "Operator": "Equals", + "Value": self._user_id, + } + ] + } + + paging = self._service.factory.create("ns5:Paging") + paging.Index = next_page_token or 0 + paging.Size = self.page_size_limit + return { + "PageInfo": paging, + "Predicates": predicates, + "ReturnAdditionalFields": self.additional_fields, + } + + +class Campaigns(BingAdsCampaignManagementStream): + """ + Gets the campaigns for all provided accounts. + API doc: https://docs.microsoft.com/en-us/advertising/campaign-management-service/getcampaignsbyaccountid?view=bingads-13 + Campaign schema: https://docs.microsoft.com/en-us/advertising/campaign-management-service/campaign?view=bingads-13 + Stream caches incoming responses to be able to reuse this data in AdGroups stream + """ + + primary_key = "Id" + # Stream caches incoming responses to avoid duplicated http requests + use_cache: bool = True + data_field: str = "Campaign" + operation_name: str = "GetCampaignsByAccountId" + additional_fields: Iterable[str] = [ + "AdScheduleUseSearcherTimeZone", + "BidStrategyId", + "CpvCpmBiddingScheme", + "DynamicDescriptionSetting", + "DynamicFeedSetting", + "MaxConversionValueBiddingScheme", + "MultimediaAdsBidAdjustment", + "TargetImpressionShareBiddingScheme", + "TargetSetting", + "VerifiedTrackingSetting", + ] + campaign_types: Iterable[str] = ["Audience", "DynamicSearchAds", "Search", "Shopping"] + + parent_key_to_foreign_key_map = { + "AccountId": "account_id", + "CustomerId": "customer_id", + } + + def request_params( + self, + stream_slice: Mapping[str, Any] = None, + **kwargs: Mapping[str, Any], + ) -> MutableMapping[str, Any]: + return { + "AccountId": stream_slice["account_id"], + "CampaignType": " ".join(self.campaign_types), + "ReturnAdditionalFields": " ".join(self.additional_fields), + } + + def stream_slices( + self, + **kwargs: Mapping[str, Any], + ) -> Iterable[Optional[Mapping[str, Any]]]: + for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): + yield {"account_id": account["Id"], "customer_id": account["ParentCustomerId"]} + + +class AdGroups(BingAdsCampaignManagementStream): + """ + Gets the ad groups for all provided accounts. + API doc: https://docs.microsoft.com/en-us/advertising/campaign-management-service/getadgroupsbycampaignid?view=bingads-13 + AdGroup schema: https://docs.microsoft.com/en-us/advertising/campaign-management-service/adgroup?view=bingads-13 + Stream caches incoming responses to be able to reuse this data in Ads stream + """ + + primary_key = "Id" + # Stream caches incoming responses to avoid duplicated http requests + use_cache: bool = True + data_field: str = "AdGroup" + operation_name: str = "GetAdGroupsByCampaignId" + additional_fields: str = "AdGroupType AdScheduleUseSearcherTimeZone CpmBid CpvBid MultimediaAdsBidAdjustment" + + parent_key_to_foreign_key_map = {"CampaignId": "campaign_id", "AccountId": "account_id", "CustomerId": "customer_id"} + + def request_params( + self, + stream_slice: Mapping[str, Any] = None, + **kwargs: Mapping[str, Any], + ) -> MutableMapping[str, Any]: + return {"CampaignId": stream_slice["campaign_id"], "ReturnAdditionalFields": self.additional_fields} + + def stream_slices( + self, + **kwargs: Mapping[str, Any], + ) -> Iterable[Optional[Mapping[str, Any]]]: + campaigns = Campaigns(self.client, self.config) + for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): + for campaign in campaigns.read_records( + sync_mode=SyncMode.full_refresh, stream_slice={"account_id": account["Id"], "customer_id": account["ParentCustomerId"]} + ): + yield {"campaign_id": campaign["Id"], "account_id": account["Id"], "customer_id": account["ParentCustomerId"]} + + +class Ads(BingAdsCampaignManagementStream): + """ + Retrieves the ads for all provided accounts. + API doc: https://docs.microsoft.com/en-us/advertising/campaign-management-service/getadsbyadgroupid?view=bingads-13 + Ad schema: https://docs.microsoft.com/en-us/advertising/campaign-management-service/ad?view=bingads-13 + """ + + primary_key = "Id" + data_field: str = "Ad" + operation_name: str = "GetAdsByAdGroupId" + additional_fields: str = "ImpressionTrackingUrls Videos LongHeadlines" + ad_types: Iterable[str] = [ + "Text", + "Image", + "Product", + "AppInstall", + "ExpandedText", + "DynamicSearch", + "ResponsiveAd", + "ResponsiveSearch", + ] + + parent_key_to_foreign_key_map = {"AdGroupId": "ad_group_id", "AccountId": "account_id", "CustomerId": "customer_id"} + + def request_params( + self, + stream_slice: Mapping[str, Any] = None, + **kwargs: Mapping[str, Any], + ) -> MutableMapping[str, Any]: + return { + "AdGroupId": stream_slice["ad_group_id"], + "AdTypes": {"AdType": self.ad_types}, + "ReturnAdditionalFields": self.additional_fields, + } + + def stream_slices( + self, + **kwargs: Mapping[str, Any], + ) -> Iterable[Optional[Mapping[str, Any]]]: + ad_groups = AdGroups(self.client, self.config) + for slice in ad_groups.stream_slices(sync_mode=SyncMode.full_refresh): + for ad_group in ad_groups.read_records(sync_mode=SyncMode.full_refresh, stream_slice=slice): + yield {"ad_group_id": ad_group["Id"], "account_id": slice["account_id"], "customer_id": slice["customer_id"]} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/bulk_streams.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/bulk_streams.py new file mode 100644 index 0000000000000..34d13b2627ca6 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/bulk_streams.py @@ -0,0 +1,190 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# +import os +from abc import ABC, abstractmethod +from datetime import timezone +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple + +import pandas as pd +import pendulum +from airbyte_cdk.models import SyncMode +from airbyte_cdk.sources.streams import IncrementalMixin +from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer +from numpy import nan +from source_bing_ads.base_streams import Accounts, BingAdsBaseStream +from source_bing_ads.utils import transform_bulk_datetime_format_to_rfc_3339 + + +class BingAdsBulkStream(BingAdsBaseStream, IncrementalMixin, ABC): + + transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization | TransformConfig.CustomSchemaNormalization) + cursor_field = "Modified Time" + primary_key = "Id" + _state = {} + + @staticmethod + @transformer.registerCustomTransform + def custom_transform_date_rfc3339(original_value, field_schema): + if original_value and "format" in field_schema and field_schema["format"] == "date-time": + transformed_value = transform_bulk_datetime_format_to_rfc_3339(original_value) + return transformed_value + return original_value + + @property + @abstractmethod + def data_scope(self) -> List[str]: + """ + Defines scopes or types of data to download. Docs: https://learn.microsoft.com/en-us/advertising/bulk-service/datascope?view=bingads-13 + """ + + @property + @abstractmethod + def download_entities(self) -> List[str]: + """ + Defines the entities that should be downloaded. Docs: https://learn.microsoft.com/en-us/advertising/bulk-service/downloadentity?view=bingads-13 + """ + + def stream_slices( + self, + **kwargs: Mapping[str, Any], + ) -> Iterable[Optional[Mapping[str, Any]]]: + for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): + yield {"account_id": account["Id"], "customer_id": account["ParentCustomerId"]} + + @property + def state(self) -> Mapping[str, Any]: + return self._state + + @state.setter + def state(self, value: Mapping[str, Any]): + current_state_value = self._state.get(str(value["Account Id"]), {}).get(self.cursor_field, "") + if value[self.cursor_field]: + record_state_value = transform_bulk_datetime_format_to_rfc_3339(value[self.cursor_field]) + new_state_value = max(current_state_value, record_state_value) + self._state.update({str(value["Account Id"]): {self.cursor_field: new_state_value}}) + + def get_start_date(self, stream_state: Mapping[str, Any] = None, account_id: str = None) -> Optional[pendulum.DateTime]: + """ + The start_date in the query can only be specified if it is within a period of up to 30 days from today. + """ + min_available_date = pendulum.now().subtract(days=30).astimezone(tz=timezone.utc) + start_date = self.client.reports_start_date + if stream_state.get(account_id, {}).get(self.cursor_field): + start_date = pendulum.parse(stream_state[account_id][self.cursor_field]) + return start_date if start_date and start_date > min_available_date else None + + def read_records( + self, + sync_mode: SyncMode, + stream_slice: Mapping[str, Any] = None, + stream_state: Mapping[str, Any] = None, + **kwargs: Mapping[str, Any], + ) -> Iterable[Mapping[str, Any]]: + stream_state = stream_state or {} + account_id = str(stream_slice.get("account_id")) if stream_slice else None + customer_id = str(stream_slice.get("customer_id")) if stream_slice else None + + report_file_path = self.client.get_bulk_entity( + data_scope=self.data_scope, + download_entities=self.download_entities, + customer_id=customer_id, + account_id=account_id, + start_date=self.get_start_date(stream_state, account_id), + ) + for record in self.read_with_chunks(report_file_path): + record = self.transform(record, stream_slice) + yield record + self.state = record + + def read_with_chunks(self, path: str, chunk_size: int = 1024) -> Iterable[Tuple[int, Mapping[str, Any]]]: + try: + with open(path, "r") as data: + chunks = pd.read_csv(data, chunksize=chunk_size, iterator=True, dialect="unix", dtype=object) + for chunk in chunks: + chunk = chunk.replace({nan: None}).to_dict(orient="records") + for row in chunk: + if row.get("Type") not in ("Format Version", "Account"): + yield row + except pd.errors.EmptyDataError as e: + self.logger.info(f"Empty data received. {e}") + except IOError as ioe: + self.logger.fatal( + f"The IO/Error occurred while reading tmp data. Called: {path}. Stream: {self.name}", + ) + raise ioe + finally: + # remove binary tmp file, after data is read + os.remove(path) + + def transform(self, record: MutableMapping[str, Any], stream_slice: Mapping[str, Any], **kwargs) -> MutableMapping[str, Any]: + """ + Bing Ads Bulk API returns all available properties for all entities. + This method filter out only available properties. + """ + actual_record = {key: value for key, value in record.items() if key in self.get_json_schema()["properties"].keys()} + actual_record["Account Id"] = stream_slice.get("account_id") + return actual_record + + +class AppInstallAds(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/app-install-ad?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["AppInstallAds"] + + +class AppInstallAdLabels(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/app-install-ad-label?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["AppInstallAdLabels"] + + +class Labels(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/label?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["Labels"] + + +class KeywordLabels(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/keyword-label?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["KeywordLabels"] + + +class Keywords(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/keyword?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["Keywords"] + + +class CampaignLabels(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/campaign-label?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["CampaignLabels"] + + +class AdGroupLabels(BingAdsBulkStream): + """ + https://learn.microsoft.com/en-us/advertising/bulk-service/ad-group-label?view=bingads-13 + """ + + data_scope = ["EntityData"] + download_entities = ["AdGroupLabels"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py new file mode 100644 index 0000000000000..2be5dd8908347 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/report_streams.py @@ -0,0 +1,748 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +import re +import xml.etree.ElementTree as ET +from abc import ABC, abstractmethod +from datetime import datetime +from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union +from urllib.parse import urlparse + +import _csv +import pendulum +from airbyte_cdk.sources.streams.core import package_name_from_class +from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader +from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer +from airbyte_protocol.models import SyncMode +from bingads import ServiceClient +from bingads.v13.internal.reporting.row_report import _RowReport +from bingads.v13.internal.reporting.row_report_iterator import _RowReportRecord +from bingads.v13.reporting import ReportingDownloadParameters +from source_bing_ads.base_streams import Accounts, BingAdsStream +from source_bing_ads.utils import transform_date_format_to_rfc_3339, transform_report_hourly_datetime_format_to_rfc_3339 +from suds import WebFault, sudsobject + + +class HourlyReportTransformerMixin: + transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization | TransformConfig.CustomSchemaNormalization) + + @staticmethod + @transformer.registerCustomTransform + def custom_transform_datetime_rfc3339(original_value, field_schema): + if original_value and "format" in field_schema and field_schema["format"] == "date-time": + print(original_value) + transformed_value = transform_report_hourly_datetime_format_to_rfc_3339(original_value) + return transformed_value + return original_value + + +class BingAdsReportingServiceStream(BingAdsStream, ABC): + # The directory where the file with report will be downloaded. + file_directory: str = "/tmp" + # timeout for reporting download operations in milliseconds + timeout: int = 300000 + report_file_format: str = "Csv" + + transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) + primary_key: List[str] = ["TimePeriod", "Network", "DeviceType"] + + cursor_field = "TimePeriod" + service_name: str = "ReportingService" + operation_name: str = "download_report" + + def get_json_schema(self) -> Mapping[str, Any]: + return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema(self.report_schema_name) + + @property + @abstractmethod + def report_name(self) -> str: + """ + Specifies bing ads report naming + """ + + @property + @abstractmethod + def report_aggregation(self) -> Optional[str]: + """ + Specifies bing ads report aggregation type + Supported types: Hourly, Daily, Weekly, Monthly + """ + + @property + @abstractmethod + def report_schema_name(self) -> str: + """ + Specifies file name with schema + """ + + @property + def default_time_periods(self): + # used when reports start date is not provided + return ["LastYear", "ThisYear"] if self.report_aggregation not in ("DayOfWeek", "HourOfDay") else ["ThisYear"] + + @property + def report_columns(self) -> Iterable[str]: + return list(self.get_json_schema().get("properties", {}).keys()) + + def parse_response(self, response: sudsobject.Object, **kwargs: Mapping[str, Any]) -> Iterable[Mapping]: + if response is not None: + try: + for row in response.report_records: + yield {column: self.get_column_value(row, column) for column in self.report_columns} + except _csv.Error as e: + self.logger.warning(f"CSV report file for stream `{self.name}` is broken or cannot be read correctly: {e}, skipping ...") + + def get_column_value(self, row: _RowReportRecord, column: str) -> Union[str, None, int, float]: + """ + Reads field value from row and transforms: + 1. empty values to logical None + 2. Percent values to numeric string e.g. "12.25%" -> "12.25" + """ + value = row.value(column) + if not value or value == "--": + return None + if "%" in value: + value = value.replace("%", "") + if value and set(self.get_json_schema()["properties"].get(column, {}).get("type")) & {"integer", "number"}: + value = value.replace(",", "") + return value + + def get_request_date(self, reporting_service: ServiceClient, date: datetime) -> sudsobject.Object: + """ + Creates XML Date object based on datetime. + https://docs.microsoft.com/en-us/advertising/reporting-service/date?view=bingads-13 + The [suds.client.Factory-class.html factory] namespace provides a factory that may be used + to create instances of objects and types defined in the WSDL. + """ + request_date = reporting_service.factory.create("Date") + request_date.Day = date.day + request_date.Month = date.month + request_date.Year = date.year + return request_date + + def request_params( + self, stream_state: Mapping[str, Any] = None, account_id: str = None, **kwargs: Mapping[str, Any] + ) -> Mapping[str, Any]: + stream_slice = kwargs["stream_slice"] + start_date = self.get_start_date(stream_state, account_id) + + reporting_service = self.client.get_service("ReportingService") + request_time_zone = reporting_service.factory.create("ReportTimeZone") + + report_time = reporting_service.factory.create("ReportTime") + report_time.ReportTimeZone = request_time_zone.GreenwichMeanTimeDublinEdinburghLisbonLondon + if start_date: + report_time.CustomDateRangeStart = self.get_request_date(reporting_service, start_date) + report_time.CustomDateRangeEnd = self.get_request_date(reporting_service, datetime.utcnow()) + report_time.PredefinedTime = None + else: + report_time.CustomDateRangeStart = None + report_time.CustomDateRangeEnd = None + report_time.PredefinedTime = stream_slice["time_period"] + + report_request = self.get_report_request(account_id, False, False, False, self.report_file_format, False, report_time) + + return { + "report_request": report_request, + "result_file_directory": self.file_directory, + "result_file_name": self.report_name, + "overwrite_result_file": True, + "timeout_in_milliseconds": self.timeout, + } + + def get_start_date(self, stream_state: Mapping[str, Any] = None, account_id: str = None): + if stream_state and account_id: + if stream_state.get(account_id, {}).get(self.cursor_field): + return pendulum.parse(self.get_report_record_timestamp(stream_state[account_id][self.cursor_field])) + + return self.client.reports_start_date + + def get_updated_state( + self, + current_stream_state: MutableMapping[str, Any], + latest_record: Mapping[str, Any], + ) -> Mapping[str, Any]: + account_id = str(latest_record["AccountId"]) + current_stream_state[account_id] = current_stream_state.get(account_id, {}) + current_stream_state[account_id][self.cursor_field] = max( + self.get_report_record_timestamp(latest_record[self.cursor_field]), + current_stream_state.get(account_id, {}).get(self.cursor_field, ""), + ) + return current_stream_state + + def send_request(self, params: Mapping[str, Any], customer_id: str, account_id: str) -> _RowReport: + request_kwargs = { + "service_name": None, + "customer_id": customer_id, + "account_id": account_id, + "operation_name": self.operation_name, + "is_report_service": True, + "params": {"download_parameters": ReportingDownloadParameters(**params)}, + } + return self.client.request(**request_kwargs) + + def get_report_request( + self, + account_id: str, + exclude_column_headers: bool, + exclude_report_footer: bool, + exclude_report_header: bool, + report_file_format: str, + return_only_complete_data: bool, + time: sudsobject.Object, + ) -> sudsobject.Object: + reporting_service = self.client.get_service(self.service_name) + report_request = reporting_service.factory.create(f"{self.report_name}Request") + if self.report_aggregation: + report_request.Aggregation = self.report_aggregation + + report_request.ExcludeColumnHeaders = exclude_column_headers + report_request.ExcludeReportFooter = exclude_report_footer + report_request.ExcludeReportHeader = exclude_report_header + report_request.Format = report_file_format + report_request.FormatVersion = "2.0" + report_request.ReturnOnlyCompleteData = return_only_complete_data + report_request.Time = time + report_request.ReportName = self.report_name + # Defines the set of accounts and campaigns to include in the report. + scope = reporting_service.factory.create("AccountThroughCampaignReportScope") + scope.AccountIds = {"long": [account_id]} + scope.Campaigns = None + report_request.Scope = scope + + columns = reporting_service.factory.create(f"ArrayOf{self.report_name}Column") + getattr(columns, f"{self.report_name}Column").append(self.report_columns) + report_request.Columns = columns + return report_request + + def get_report_record_timestamp(self, datestring: str) -> str: + """ + Parse report date field based on aggregation type + """ + return ( + self.transformer._custom_normalizer(datestring, self.get_json_schema()["properties"][self.cursor_field]) + if self.transformer._custom_normalizer + else datestring + ) + + def stream_slices( + self, + **kwargs: Mapping[str, Any], + ) -> Iterable[Optional[Mapping[str, Any]]]: + for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): + for period in self.default_time_periods: + yield {"account_id": account["Id"], "customer_id": account["ParentCustomerId"], "time_period": period} + + +class BingAdsReportingServicePerformanceStream(BingAdsReportingServiceStream, ABC): + def get_start_date(self, stream_state: Mapping[str, Any] = None, account_id: str = None): + start_date = super().get_start_date(stream_state, account_id) + + if self.config.get("lookback_window") and start_date: + # Datetime subtract won't work with days = 0 + # it'll output an AirbyteError + return start_date.subtract(days=self.config["lookback_window"]) + else: + return start_date + + +class BudgetSummaryReport(BingAdsReportingServiceStream): + report_name: str = "BudgetSummaryReport" + report_aggregation = None + cursor_field = "Date" + report_schema_name = "budget_summary_report" + primary_key = "Date" + + transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization | TransformConfig.CustomSchemaNormalization) + + @staticmethod + @transformer.registerCustomTransform + def custom_transform_date_rfc3339(original_value, field_schema): + if original_value and "format" in field_schema and field_schema["format"] == "date": + transformed_value = transform_date_format_to_rfc_3339(original_value) + return transformed_value + return original_value + + +class CampaignPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "CampaignPerformanceReport" + + report_schema_name = "campaign_performance_report" + primary_key = [ + "AccountId", + "CampaignId", + "TimePeriod", + "CurrencyCode", + "AdDistribution", + "DeviceType", + "Network", + "DeliveredMatchType", + "DeviceOS", + "TopVsOther", + "BidMatchType", + ] + + +class CampaignPerformanceReportHourly(HourlyReportTransformerMixin, CampaignPerformanceReport): + report_aggregation = "Hourly" + + report_schema_name = "campaign_performance_report_hourly" + + +class CampaignPerformanceReportDaily(CampaignPerformanceReport): + report_aggregation = "Daily" + + +class CampaignPerformanceReportWeekly(CampaignPerformanceReport): + report_aggregation = "Weekly" + + +class CampaignPerformanceReportMonthly(CampaignPerformanceReport): + report_aggregation = "Monthly" + + +class CampaignImpressionPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + """ + https://learn.microsoft.com/en-us/advertising/reporting-service/adgroupperformancereportrequest?view=bingads-13 + Primary key cannot be set: due to included `Impression Share Performance Statistics` some fields should be removed, + see https://learn.microsoft.com/en-us/advertising/guides/reports?view=bingads-13#columnrestrictions for more info. + """ + + report_name: str = "CampaignPerformanceReport" + + report_schema_name = "campaign_impression_performance_report" + + primary_key = None + + +class CampaignImpressionPerformanceReportHourly(HourlyReportTransformerMixin, CampaignImpressionPerformanceReport): + report_aggregation = "Hourly" + + report_schema_name = "campaign_impression_performance_report_hourly" + + +class CampaignImpressionPerformanceReportDaily(CampaignImpressionPerformanceReport): + report_aggregation = "Daily" + + +class CampaignImpressionPerformanceReportWeekly(CampaignImpressionPerformanceReport): + report_aggregation = "Weekly" + + +class CampaignImpressionPerformanceReportMonthly(CampaignImpressionPerformanceReport): + report_aggregation = "Monthly" + + +class AdPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "AdPerformanceReport" + + report_schema_name = "ad_performance_report" + primary_key = [ + "AccountId", + "CampaignId", + "AdGroupId", + "AdId", + "TimePeriod", + "CurrencyCode", + "AdDistribution", + "DeviceType", + "Language", + "Network", + "DeviceOS", + "TopVsOther", + "BidMatchType", + "DeliveredMatchType", + ] + + +class AdPerformanceReportHourly(HourlyReportTransformerMixin, AdPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "ad_performance_report_hourly" + + +class AdPerformanceReportDaily(AdPerformanceReport): + report_aggregation = "Daily" + + +class AdPerformanceReportWeekly(AdPerformanceReport): + report_aggregation = "Weekly" + + +class AdPerformanceReportMonthly(AdPerformanceReport): + report_aggregation = "Monthly" + + +class AdGroupPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "AdGroupPerformanceReport" + report_schema_name = "ad_group_performance_report" + + primary_key = [ + "AccountId", + "CampaignId", + "AdGroupId", + "TimePeriod", + "CurrencyCode", + "AdDistribution", + "DeviceType", + "Network", + "DeliveredMatchType", + "DeviceOS", + "TopVsOther", + "BidMatchType", + "Language", + ] + + +class AdGroupPerformanceReportHourly(HourlyReportTransformerMixin, AdGroupPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "ad_group_performance_report_hourly" + + +class AdGroupPerformanceReportDaily(AdGroupPerformanceReport): + report_aggregation = "Daily" + + +class AdGroupPerformanceReportWeekly(AdGroupPerformanceReport): + report_aggregation = "Weekly" + + +class AdGroupPerformanceReportMonthly(AdGroupPerformanceReport): + report_aggregation = "Monthly" + + +class AdGroupImpressionPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + """ + https://learn.microsoft.com/en-us/advertising/reporting-service/adgroupperformancereportrequest?view=bingads-13 + Primary key cannot be set: due to included `Impression Share Performance Statistics` some fields should be removed, + see https://learn.microsoft.com/en-us/advertising/guides/reports?view=bingads-13#columnrestrictions for more info. + """ + + report_name: str = "AdGroupPerformanceReport" + report_schema_name = "ad_group_impression_performance_report" + + +class AdGroupImpressionPerformanceReportHourly(HourlyReportTransformerMixin, AdGroupImpressionPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "ad_group_impression_performance_report_hourly" + + +class AdGroupImpressionPerformanceReportDaily(AdGroupImpressionPerformanceReport): + report_aggregation = "Daily" + + +class AdGroupImpressionPerformanceReportWeekly(AdGroupImpressionPerformanceReport): + report_aggregation = "Weekly" + + +class AdGroupImpressionPerformanceReportMonthly(AdGroupImpressionPerformanceReport): + report_aggregation = "Monthly" + + +class KeywordPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "KeywordPerformanceReport" + report_schema_name = "keyword_performance_report" + primary_key = [ + "AccountId", + "CampaignId", + "AdGroupId", + "KeywordId", + "AdId", + "TimePeriod", + "CurrencyCode", + "DeliveredMatchType", + "AdDistribution", + "DeviceType", + "Language", + "Network", + "DeviceOS", + "TopVsOther", + "BidMatchType", + ] + + +class KeywordPerformanceReportHourly(HourlyReportTransformerMixin, KeywordPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "keyword_performance_report_hourly" + + +class KeywordPerformanceReportDaily(KeywordPerformanceReport): + report_aggregation = "Daily" + report_schema_name = "keyword_performance_report_daily" + + +class KeywordPerformanceReportWeekly(KeywordPerformanceReport): + report_aggregation = "Weekly" + + +class KeywordPerformanceReportMonthly(KeywordPerformanceReport): + report_aggregation = "Monthly" + + +class GeographicPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "GeographicPerformanceReport" + report_schema_name = "geographic_performance_report" + + # Need to override the primary key here because the one inherited from the PerformanceReportsMixin + # is incorrect for the geographic performance reports + primary_key = None + + +class GeographicPerformanceReportHourly(HourlyReportTransformerMixin, GeographicPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "geographic_performance_report_hourly" + + +class GeographicPerformanceReportDaily(GeographicPerformanceReport): + report_aggregation = "Daily" + + +class GeographicPerformanceReportWeekly(GeographicPerformanceReport): + report_aggregation = "Weekly" + + +class GeographicPerformanceReportMonthly(GeographicPerformanceReport): + report_aggregation = "Monthly" + + +class AccountPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "AccountPerformanceReport" + report_schema_name = "account_performance_report" + primary_key = [ + "AccountId", + "TimePeriod", + "CurrencyCode", + "AdDistribution", + "DeviceType", + "Network", + "DeliveredMatchType", + "DeviceOS", + "TopVsOther", + "BidMatchType", + ] + + +class AccountPerformanceReportHourly(HourlyReportTransformerMixin, AccountPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "account_performance_report_hourly" + + +class AccountPerformanceReportDaily(AccountPerformanceReport): + report_aggregation = "Daily" + + +class AccountPerformanceReportWeekly(AccountPerformanceReport): + report_aggregation = "Weekly" + + +class AccountPerformanceReportMonthly(AccountPerformanceReport): + report_aggregation = "Monthly" + + +class AccountImpressionPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + """ + Report source: https://docs.microsoft.com/en-us/advertising/reporting-service/accountperformancereportrequest?view=bingads-13 + Primary key cannot be set: due to included `Impression Share Performance Statistics` some fields should be removed, + see https://learn.microsoft.com/en-us/advertising/guides/reports?view=bingads-13#columnrestrictions for more info. + """ + + report_name: str = "AccountPerformanceReport" + report_schema_name = "account_impression_performance_report" + primary_key = None + + +class AccountImpressionPerformanceReportHourly(HourlyReportTransformerMixin, AccountImpressionPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "account_impression_performance_report_hourly" + + +class AccountImpressionPerformanceReportDaily(AccountImpressionPerformanceReport): + report_aggregation = "Daily" + + +class AccountImpressionPerformanceReportWeekly(AccountImpressionPerformanceReport): + report_aggregation = "Weekly" + + +class AccountImpressionPerformanceReportMonthly(AccountImpressionPerformanceReport): + report_aggregation = "Monthly" + + +class AgeGenderAudienceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "AgeGenderAudienceReport" + + report_schema_name = "age_gender_audience_report" + primary_key = ["AgeGroup", "Gender", "TimePeriod", "AccountId", "CampaignId", "Language", "AdDistribution"] + + +class AgeGenderAudienceReportHourly(HourlyReportTransformerMixin, AgeGenderAudienceReport): + report_aggregation = "Hourly" + report_schema_name = "age_gender_audience_report_hourly" + + +class AgeGenderAudienceReportDaily(AgeGenderAudienceReport): + report_aggregation = "Daily" + + +class AgeGenderAudienceReportWeekly(AgeGenderAudienceReport): + report_aggregation = "Weekly" + + +class AgeGenderAudienceReportMonthly(AgeGenderAudienceReport): + report_aggregation = "Monthly" + + +class SearchQueryPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "SearchQueryPerformanceReport" + report_schema_name = "search_query_performance_report" + + primary_key = [ + "SearchQuery", + "Keyword", + "TimePeriod", + "AccountId", + "CampaignId", + "Language", + "DeliveredMatchType", + "DeviceType", + "DeviceOS", + "TopVsOther", + ] + + +class SearchQueryPerformanceReportHourly(HourlyReportTransformerMixin, SearchQueryPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "search_query_performance_report_hourly" + + +class SearchQueryPerformanceReportDaily(SearchQueryPerformanceReport): + report_aggregation = "Daily" + + +class SearchQueryPerformanceReportWeekly(SearchQueryPerformanceReport): + report_aggregation = "Weekly" + + +class SearchQueryPerformanceReportMonthly(SearchQueryPerformanceReport): + report_aggregation = "Monthly" + + +class UserLocationPerformanceReport(BingAdsReportingServicePerformanceStream, ABC): + report_name: str = "UserLocationPerformanceReport" + report_schema_name = "user_location_performance_report" + primary_key = [ + "AccountId", + "AdGroupId", + "CampaignId", + "DeliveredMatchType", + "DeviceOS", + "DeviceType", + "Language", + "LocationId", + "QueryIntentLocationId", + "TimePeriod", + "TopVsOther", + ] + + +class UserLocationPerformanceReportHourly(HourlyReportTransformerMixin, UserLocationPerformanceReport): + report_aggregation = "Hourly" + report_schema_name = "user_location_performance_report_hourly" + + +class UserLocationPerformanceReportDaily(UserLocationPerformanceReport): + report_aggregation = "Daily" + + +class UserLocationPerformanceReportWeekly(UserLocationPerformanceReport): + report_aggregation = "Weekly" + + +class UserLocationPerformanceReportMonthly(UserLocationPerformanceReport): + report_aggregation = "Monthly" + + +class CustomReport(BingAdsReportingServicePerformanceStream, ABC): + transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) + custom_report_columns = [] + report_schema_name = None + primary_key = None + + @property + def cursor_field(self) -> Union[str, List[str]]: + # Summary aggregation doesn't include TimePeriod field + if self.report_aggregation not in ("Summary", "DayOfWeek", "HourOfDay"): + return "TimePeriod" + + @property + def report_columns(self): + # adding common and default columns + if "AccountId" not in self.custom_report_columns: + self.custom_report_columns.append("AccountId") + if self.cursor_field and self.cursor_field not in self.custom_report_columns: + self.custom_report_columns.append(self.cursor_field) + return list(frozenset(self.custom_report_columns)) + + def get_json_schema(self) -> Mapping[str, Any]: + columns_schema = {col: {"type": ["null", "string"]} for col in self.report_columns} + schema: Mapping[str, Any] = { + "$schema": "https://json-schema.org/draft-07/schema#", + "type": ["null", "object"], + "additionalProperties": True, + "properties": columns_schema, + } + return schema + + def validate_report_configuration(self) -> Tuple[bool, str]: + # gets /bingads/v13/proxies/production/reporting_service.xml + reporting_service_file = self.client.get_service(self.service_name)._get_service_info_dict(self.client.api_version)[ + ("reporting", self.client.environment) + ] + tree = ET.parse(urlparse(reporting_service_file).path) + request_object = tree.find(f".//{{*}}complexType[@name='{self.report_name}Request']") + + report_object_columns = self._get_object_columns(request_object, tree) + is_custom_cols_in_report_object_cols = all(x in report_object_columns for x in self.custom_report_columns) + + if not is_custom_cols_in_report_object_cols: + return False, ( + f"Reporting Columns are invalid. Columns that you provided don't belong to Reporting Data Object Columns:" + f" {self.custom_report_columns}. Please ensure it is correct in Bing Ads Docs." + ) + + return True, "" + + def _clear_namespace(self, type: str) -> str: + return re.sub(r"^[a-z]+:", "", type) + + def _get_object_columns(self, request_el: ET.Element, tree: ET.ElementTree) -> List[str]: + column_el = request_el.find(".//{*}element[@name='Columns']") + array_of_columns_name = self._clear_namespace(column_el.get("type")) + + array_of_columns_elements = tree.find(f".//{{*}}complexType[@name='{array_of_columns_name}']") + inner_array_of_columns_elements = array_of_columns_elements.find(".//{*}element") + column_el_name = self._clear_namespace(inner_array_of_columns_elements.get("type")) + + column_el = tree.find(f".//{{*}}simpleType[@name='{column_el_name}']") + column_enum_items = column_el.findall(".//{*}enumeration") + column_enum_items_values = [el.get("value") for el in column_enum_items] + return column_enum_items_values + + def get_report_record_timestamp(self, datestring: str) -> str: + """ + Parse report date field based on aggregation type + """ + if not self.report_aggregation or self.report_aggregation == "Summary": + datestring = transform_date_format_to_rfc_3339(datestring) + elif self.report_aggregation == "Hourly": + datestring = transform_report_hourly_datetime_format_to_rfc_3339(datestring) + return datestring + + def send_request(self, params: Mapping[str, Any], customer_id: str, account_id: str) -> _RowReport: + try: + return super().send_request(params, customer_id, account_id) + except WebFault as e: + self.logger.error( + f"Could not sync custom report {self.name}: Please validate your column and aggregation configuration. " + f"Error form server: [{e.fault.faultstring}]" + ) diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/reports.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/reports.py deleted file mode 100644 index 731954bad986d..0000000000000 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/reports.py +++ /dev/null @@ -1,336 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from abc import ABC, abstractmethod -from datetime import datetime -from typing import Any, Iterable, List, Mapping, MutableMapping, Optional - -import pendulum -import source_bing_ads.source -from airbyte_cdk.models import SyncMode -from airbyte_cdk.sources.streams.core import package_name_from_class -from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader -from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer -from bingads.service_client import ServiceClient -from bingads.v13.internal.reporting.row_report import _RowReport -from bingads.v13.reporting import ReportingDownloadParameters -from suds import sudsobject - -AVERAGE_FIELD_TYPES = { - "AverageCpc": "number", - "AveragePosition": "number", - "AverageCpm": "number", -} -AVERAGE_FIELDS = list(AVERAGE_FIELD_TYPES.keys()) - -CONVERSION_FIELD_TYPES = { - "Conversions": "number", - "ConversionRate": "number", - "ConversionsQualified": "number", -} -CONVERSION_FIELDS = list(CONVERSION_FIELD_TYPES.keys()) - -ALL_CONVERSION_FIELD_TYPES = { - "AllConversions": "integer", - "AllConversionRate": "number", -} -ALL_CONVERSION_FIELDS = list(ALL_CONVERSION_FIELD_TYPES.keys()) - -LOW_QUALITY_FIELD_TYPES = { - "LowQualityClicks": "integer", - "LowQualityClicksPercent": "number", - "LowQualityImpressions": "integer", - "LowQualitySophisticatedClicks": "integer", - "LowQualityConversions": "integer", - "LowQualityConversionRate": "number", -} -LOW_QUALITY_FIELDS = list(LOW_QUALITY_FIELD_TYPES.keys()) - -REVENUE_FIELD_TYPES = { - "Revenue": "number", - "RevenuePerConversion": "number", - "RevenuePerAssist": "number", -} -REVENUE_FIELDS = list(REVENUE_FIELD_TYPES.keys()) - -ALL_REVENUE_FIELD_TYPES = { - "AllRevenue": "number", - "AllRevenuePerConversion": "number", -} -ALL_REVENUE_FIELDS = list(ALL_REVENUE_FIELD_TYPES.keys()) - -IMPRESSION_FIELD_TYPES = { - "ImpressionSharePercent": "number", - "ImpressionLostToBudgetPercent": "number", - "ImpressionLostToRankAggPercent": "number", -} -IMPRESSION_FIELDS = list(IMPRESSION_FIELD_TYPES.keys()) - - -HISTORICAL_FIELD_TYPES = { - "HistoricalQualityScore": "number", - "HistoricalExpectedCtr": "number", - "HistoricalAdRelevance": "number", - "HistoricalLandingPageExperience": "number", -} -HISTORICAL_FIELDS = list(HISTORICAL_FIELD_TYPES.keys()) - -BUDGET_FIELD_TYPES = { - "BudgetName": "string", - "BudgetStatus": "string", - "BudgetAssociationStatus": "string", -} -BUDGET_FIELDS = list(BUDGET_FIELD_TYPES.keys()) - -REPORT_FIELD_TYPES = { - "AccountId": "integer", - "AdId": "integer", - "AdGroupCriterionId": "integer", - "AdGroupId": "integer", - "AdRelevance": "number", - "Assists": "integer", - "AllCostPerConversion": "number", - "AllReturnOnAdSpend": "number", - "BusinessCategoryId": "integer", - "BusinessListingId": "integer", - "CampaignId": "integer", - "ClickCalls": "integer", - "Clicks": "integer", - "CostPerAssist": "number", - "CostPerConversion": "number", - "Ctr": "number", - "CurrentMaxCpc": "number", - "EstimatedClickPercent": "number", - "EstimatedClicks": "integer", - "EstimatedConversionRate": "number", - "EstimatedConversions": "integer", - "EstimatedCtr": "number", - "EstimatedImpressionPercent": "number", - "EstimatedImpressions": "integer", - "ExactMatchImpressionSharePercent": "number", - "Impressions": "integer", - "ImpressionSharePercent": "number", - "KeywordId": "integer", - "LandingPageExperience": "number", - "PhoneCalls": "integer", - "PhoneImpressions": "integer", - "Ptr": "number", - "QualityImpact": "number", - "QualityScore": "number", - "ReturnOnAdSpend": "number", - "SidebarBid": "number", - "Spend": "number", - "MonthlyBudget": "number", - "DailySpend": "number", - "MonthToDateSpend": "number", - "AbsoluteTopImpressionRatePercent": "number", - "ViewThroughConversions": "integer", - "ViewThroughConversionsQualified": "number", - "MainlineBid": "number", - "Mainline1Bid": "number", - "FirstPageBid": "number", - **AVERAGE_FIELD_TYPES, - **CONVERSION_FIELD_TYPES, - **ALL_CONVERSION_FIELD_TYPES, - **LOW_QUALITY_FIELD_TYPES, - **REVENUE_FIELD_TYPES, - **ALL_REVENUE_FIELD_TYPES, - **IMPRESSION_FIELD_TYPES, - **HISTORICAL_FIELD_TYPES, - **BUDGET_FIELD_TYPES, -} - - -class ReportsMixin(ABC): - # The directory where the file with report will be downloaded. - file_directory: str = "/tmp" - # timeout for reporting download operations in milliseconds - timeout: int = 300000 - report_file_format: str = "Csv" - # used when reports start date is not provided - time_periods = ["LastYear", "ThisYear"] - - transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) - primary_key: List[str] = ["TimePeriod", "Network", "DeviceType"] - - @property - @abstractmethod - def report_name(self) -> str: - """ - Specifies bing ads report naming - """ - pass - - @property - @abstractmethod - def report_columns(self) -> Iterable[str]: - """ - Specifies bing ads report naming - TODO: refactor to use list(self.get_json_schema().get("properties", {}).keys()), see AgeGenderAudienceReport - """ - pass - - @property - @abstractmethod - def report_aggregation(self) -> Optional[str]: - """ - Specifies bing ads report aggregation type - Supported types: Hourly, Daily, Weekly, Monthly - """ - pass - - @property - @abstractmethod - def report_schema_name(self) -> str: - """ - Specifies file name with schema - """ - pass - - def get_json_schema(self) -> Mapping[str, Any]: - return ResourceSchemaLoader(package_name_from_class(self.__class__)).get_schema(self.report_schema_name) - - def get_request_date(self, reporting_service: ServiceClient, date: datetime) -> sudsobject.Object: - """ - Creates XML Date object based on datetime. - https://docs.microsoft.com/en-us/advertising/reporting-service/date?view=bingads-13 - The [suds.client.Factory-class.html factory] namespace provides a factory that may be used - to create instances of objects and types defined in the WSDL. - """ - request_date = reporting_service.factory.create("Date") - request_date.Day = date.day - request_date.Month = date.month - request_date.Year = date.year - return request_date - - def request_params( - self, stream_state: Mapping[str, Any] = None, account_id: str = None, **kwargs: Mapping[str, Any] - ) -> Mapping[str, Any]: - stream_slice = kwargs["stream_slice"] - start_date = self.get_start_date(stream_state, account_id) - - reporting_service = self.client.get_service("ReportingService") - request_time_zone = reporting_service.factory.create("ReportTimeZone") - - report_time = reporting_service.factory.create("ReportTime") - report_time.ReportTimeZone = request_time_zone.GreenwichMeanTimeDublinEdinburghLisbonLondon - if start_date: - report_time.CustomDateRangeStart = self.get_request_date(reporting_service, start_date) - report_time.CustomDateRangeEnd = self.get_request_date(reporting_service, datetime.utcnow()) - report_time.PredefinedTime = None - else: - report_time.CustomDateRangeStart = None - report_time.CustomDateRangeEnd = None - report_time.PredefinedTime = stream_slice["time_period"] - - report_request = self.get_report_request(account_id, False, False, False, self.report_file_format, False, report_time) - - return { - "report_request": report_request, - "result_file_directory": self.file_directory, - "result_file_name": self.report_name, - "overwrite_result_file": True, - "timeout_in_milliseconds": self.timeout, - } - - def get_start_date(self, stream_state: Mapping[str, Any] = None, account_id: str = None): - if stream_state and account_id: - if stream_state.get(account_id, {}).get(self.cursor_field): - return pendulum.from_timestamp(stream_state[account_id][self.cursor_field]) - - return self.client.reports_start_date - - def get_updated_state( - self, - current_stream_state: MutableMapping[str, Any], - latest_record: Mapping[str, Any], - ) -> Mapping[str, Any]: - account_id = str(latest_record["AccountId"]) - current_stream_state[account_id] = current_stream_state.get(account_id, {}) - current_stream_state[account_id][self.cursor_field] = max( - self.get_report_record_timestamp(latest_record[self.cursor_field]), - current_stream_state.get(account_id, {}).get(self.cursor_field, 1), - ) - return current_stream_state - - def send_request(self, params: Mapping[str, Any], customer_id: str, account_id: str) -> _RowReport: - request_kwargs = { - "service_name": None, - "customer_id": customer_id, - "account_id": account_id, - "operation_name": self.operation_name, - "is_report_service": True, - "params": {"download_parameters": ReportingDownloadParameters(**params)}, - } - return self.client.request(**request_kwargs) - - def get_report_request( - self, - account_id: str, - exclude_column_headers: bool, - exclude_report_footer: bool, - exclude_report_header: bool, - report_file_format: str, - return_only_complete_data: bool, - time: sudsobject.Object, - ) -> sudsobject.Object: - reporting_service = self.client.get_service(self.service_name) - report_request = reporting_service.factory.create(f"{self.report_name}Request") - if self.report_aggregation: - report_request.Aggregation = self.report_aggregation - - report_request.ExcludeColumnHeaders = exclude_column_headers - report_request.ExcludeReportFooter = exclude_report_footer - report_request.ExcludeReportHeader = exclude_report_header - report_request.Format = report_file_format - report_request.FormatVersion = "2.0" - report_request.ReturnOnlyCompleteData = return_only_complete_data - report_request.Time = time - report_request.ReportName = self.report_name - # Defines the set of accounts and campaigns to include in the report. - scope = reporting_service.factory.create("AccountThroughCampaignReportScope") - scope.AccountIds = {"long": [account_id]} - scope.Campaigns = None - report_request.Scope = scope - - columns = reporting_service.factory.create(f"ArrayOf{self.report_name}Column") - getattr(columns, f"{self.report_name}Column").append(self.report_columns) - report_request.Columns = columns - return report_request - - def get_report_record_timestamp(self, datestring: str) -> int: - """ - Parse report date field based on aggregation type - """ - if not self.report_aggregation: - date = pendulum.from_format(datestring, "M/D/YYYY") - else: - if self.report_aggregation == "Hourly": - date = pendulum.from_format(datestring, "YYYY-MM-DD|H") - else: - date = pendulum.parse(datestring) - - return date.int_timestamp - - def stream_slices( - self, - **kwargs: Mapping[str, Any], - ) -> Iterable[Optional[Mapping[str, Any]]]: - for account in source_bing_ads.source.Accounts(self.client, self.config).read_records(SyncMode.full_refresh): - for period in self.time_periods: - yield {"account_id": account["Id"], "customer_id": account["ParentCustomerId"], "time_period": period} - - yield from [] - - -class PerformanceReportsMixin(ReportsMixin): - def get_start_date(self, stream_state: Mapping[str, Any] = None, account_id: str = None): - start_date = super().get_start_date(stream_state, account_id) - - if self.config.get("lookback_window") and start_date: - # Datetime subtract won't work with days = 0 - # it'll output an AirbyteError - return start_date.subtract(days=self.config["lookback_window"]) - else: - return start_date diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report.json index 530308e88e989..f2ce126eff0d8 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report.json @@ -12,7 +12,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CurrencyCode": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report_hourly.json index 253549bbbdf47..7c0cff82f7796 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report_hourly.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_impression_performance_report_hourly.json @@ -12,7 +12,9 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "CurrencyCode": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report.json index 27f13fb2f71ce..ab5c273877ab8 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report.json @@ -6,7 +6,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CurrencyCode": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report_hourly.json new file mode 100644 index 0000000000000..4013dd037402a --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/account_performance_report_hourly.json @@ -0,0 +1,122 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "AccountNumber": { + "type": ["null", "string"] + }, + "PhoneImpressions": { + "type": ["null", "integer"] + }, + "PhoneCalls": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "Ptr": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "LowQualityClicks": { + "type": ["null", "integer"] + }, + "LowQualityClicksPercent": { + "type": ["null", "number"] + }, + "LowQualityImpressions": { + "type": ["null", "integer"] + }, + "LowQualitySophisticatedClicks": { + "type": ["null", "integer"] + }, + "LowQualityConversions": { + "type": ["null", "integer"] + }, + "LowQualityConversionRate": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/accounts.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/accounts.json index 858ecfe2d9bc3..63bf3d699add3 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/accounts.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/accounts.json @@ -27,7 +27,15 @@ "type": ["null", "string"] }, "LinkedAgencies": { - "type": ["null", "string"] + "type": ["null", "object"], + "properties": { + "Id": { + "type": ["null", "integer"] + }, + "Name": { + "type": ["null", "string"] + } + } }, "TaxInformation": { "type": ["null", "string"] @@ -89,7 +97,9 @@ "type": ["null", "number"] }, "LastModifiedTime": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_without_timezone" }, "Name": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report.json index 7db370e256d22..beee977875e88 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report.json @@ -12,7 +12,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report_hourly.json index 272a62bd655da..9ebbe2b60f2f1 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report_hourly.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_impression_performance_report_hourly.json @@ -12,7 +12,9 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_labels.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_labels.json index 6cb4d85c33530..5daab1862dbd4 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_labels.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_labels.json @@ -21,7 +21,9 @@ "type": ["null", "integer"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report.json index b0e952bb0aecb..6b8d28e98867c 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report.json @@ -12,7 +12,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CurrencyCode": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report_hourly.json new file mode 100644 index 0000000000000..a1300b327c695 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_group_performance_report_hourly.json @@ -0,0 +1,158 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "Language": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "CampaignType": { + "type": ["null", "string"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "AdGroupType": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "QualityScore": { + "type": ["null", "number"] + }, + "ExpectedCtr": { + "type": ["null", "string"] + }, + "AdRelevance": { + "type": ["null", "number"] + }, + "LandingPageExperience": { + "type": ["null", "number"] + }, + "PhoneImpressions": { + "type": ["null", "integer"] + }, + "PhoneCalls": { + "type": ["null", "integer"] + }, + "Ptr": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "CustomParameters": { + "type": ["null", "string"] + }, + "FinalUrlSuffix": { + "type": ["null", "string"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report.json index 44945b40758ec..c884c8e5ffb33 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report.json @@ -15,7 +15,14 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" + }, + "AbsoluteTopImpressionRatePercent": { + "type": ["null", "number"] + }, + "TopImpressionRatePercent": { + "type": ["null", "number"] }, "CurrencyCode": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report_hourly.json new file mode 100644 index 0000000000000..93a690f08cd33 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/ad_performance_report_hourly.json @@ -0,0 +1,152 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "AdId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Language": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "CampaignType": { + "type": ["null", "string"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "DestinationUrl": { + "type": ["null", "string"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "CustomParameters": { + "type": ["null", "string"] + }, + "FinalAppUrl": { + "type": ["null", "string"] + }, + "AdDescription": { + "type": ["null", "string"] + }, + "AdDescription2": { + "type": ["null", "string"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "ViewThroughConversionsQualified": { + "type": ["null", "number"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report.json index 29b9b94cb932a..36285f85a5e21 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report.json @@ -12,7 +12,8 @@ "type": ["null", "string"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "AllConversions": { "type": ["null", "integer"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report_hourly.json new file mode 100644 index 0000000000000..544559e884d57 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/age_gender_audience_report_hourly.json @@ -0,0 +1,110 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "AgeGroup": { + "type": ["null", "string"] + }, + "Gender": { + "type": ["null", "string"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "AccountNumber": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "ExtendedCost": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "Language": { + "type": ["null", "string"] + }, + "AccountStatus": { + "type": ["null", "string"] + }, + "CampaignStatus": { + "type": ["null", "string"] + }, + "AdGroupStatus": { + "type": ["null", "string"] + }, + "BaseCampaignId": { + "type": ["null", "string"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "Goal": { + "type": ["null", "string"] + }, + "GoalType": { + "type": ["null", "string"] + }, + "AbsoluteTopImpressionRatePercent": { + "type": ["null", "number"] + }, + "TopImpressionRatePercent": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AllConversionsQualified": { + "type": ["null", "number"] + }, + "ViewThroughConversionsQualified": { + "type": ["null", "number"] + }, + "ViewThroughRevenue": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ad_labels.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ad_labels.json index 37bd0ad224a46..74ebe7d23dfe1 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ad_labels.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ad_labels.json @@ -15,7 +15,9 @@ "type": ["null", "integer"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ads.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ads.json index b409f06d62cdf..4db96b2540201 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ads.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/app_install_ads.json @@ -45,7 +45,9 @@ "type": ["null", "integer"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Parent Id": { "type": ["null", "integer"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/budget_summary_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/budget_summary_report.json index f8366861a8c49..6c4cf7f5a9393 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/budget_summary_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/budget_summary_report.json @@ -18,7 +18,8 @@ "type": ["null", "string"] }, "Date": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "MonthlyBudget": { "type": ["null", "number"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report.json index 9390d3de29671..4b2983bd3258f 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report.json @@ -12,7 +12,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CampaignStatus": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report_hourly.json index 90a90d070b7fe..a5e48498383f1 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report_hourly.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_impression_performance_report_hourly.json @@ -12,7 +12,9 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "CampaignStatus": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_labels.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_labels.json index e5fdf2701befb..7db5d82599fa7 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_labels.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_labels.json @@ -18,7 +18,9 @@ "type": ["null", "integer"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report.json index eff183e144e9b..6ac3320854d66 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report.json @@ -9,7 +9,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CurrencyCode": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report_hourly.json new file mode 100644 index 0000000000000..bda17310348f8 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaign_performance_report_hourly.json @@ -0,0 +1,176 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "CampaignType": { + "type": ["null", "string"] + }, + "CampaignStatus": { + "type": ["null", "string"] + }, + "CampaignLabels": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "QualityScore": { + "type": ["null", "number"] + }, + "AdRelevance": { + "type": ["null", "number"] + }, + "LandingPageExperience": { + "type": ["null", "number"] + }, + "PhoneImpressions": { + "type": ["null", "integer"] + }, + "PhoneCalls": { + "type": ["null", "integer"] + }, + "Ptr": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "CustomParameters": { + "type": ["null", "string"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "LowQualityClicks": { + "type": ["null", "integer"] + }, + "LowQualityClicksPercent": { + "type": ["null", "number"] + }, + "LowQualityImpressions": { + "type": ["null", "integer"] + }, + "LowQualitySophisticatedClicks": { + "type": ["null", "integer"] + }, + "LowQualityConversions": { + "type": ["null", "integer"] + }, + "LowQualityConversionRate": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + }, + "BudgetName": { + "type": ["null", "string"] + }, + "BudgetStatus": { + "type": ["null", "string"] + }, + "BudgetAssociationStatus": { + "type": ["null", "string"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaigns.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaigns.json index 6721b299b2635..c5790cd8928f3 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaigns.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/campaigns.json @@ -21,7 +21,7 @@ "type": ["null", "object"], "properties": { "Amount": { - "type": ["null", "string"] + "type": ["null", "number"] } } } diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report.json index f2c65f08ff39a..8e55c0a611213 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report.json @@ -13,7 +13,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "AccountNumber": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report_hourly.json new file mode 100644 index 0000000000000..9b79a66cfe249 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/geographic_performance_report_hourly.json @@ -0,0 +1,213 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "AccountNumber": { + "type": ["null", "string"] + }, + "Country": { + "type": ["null", "string"] + }, + "State": { + "type": ["null", "string"] + }, + "MetroArea": { + "type": ["null", "string"] + }, + "City": { + "type": ["null", "string"] + }, + "ProximityTargetLocation": { + "type": ["null", "string"] + }, + "Radius": { + "type": ["null", "string"] + }, + "LocationType": { + "type": ["null", "string"] + }, + "MostSpecificLocation": { + "type": ["null", "string"] + }, + "AccountStatus": { + "type": ["null", "string"] + }, + "CampaignStatus": { + "type": ["null", "string"] + }, + "AdGroupStatus": { + "type": ["null", "string"] + }, + "County": { + "type": ["null", "string"] + }, + "PostalCode": { + "type": ["null", "string"] + }, + "LocationId": { + "type": ["null", "string"] + }, + "BaseCampaignId": { + "type": ["null", "string"] + }, + "Goal": { + "type": ["null", "string"] + }, + "GoalType": { + "type": ["null", "string"] + }, + "AbsoluteTopImpressionRatePercent": { + "type": ["null", "number"] + }, + "TopImpressionRatePercent": { + "type": ["null", "string"] + }, + "AllConversionsQualified": { + "type": ["null", "string"] + }, + "Neighborhood": { + "type": ["null", "string"] + }, + "ViewThroughRevenue": { + "type": ["null", "string"] + }, + "CampaignType": { + "type": ["null", "string"] + }, + "AssetGroupId": { + "type": ["null", "string"] + }, + "AssetGroupName": { + "type": ["null", "string"] + }, + "AssetGroupStatus": { + "type": ["null", "string"] + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Language": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "ViewThroughConversionsQualified": { + "type": ["null", "number"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_labels.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_labels.json index 37bd0ad224a46..74ebe7d23dfe1 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_labels.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_labels.json @@ -15,7 +15,9 @@ "type": ["null", "integer"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report.json index c6c6279fbde0a..70ccc68a0b93d 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report.json @@ -22,7 +22,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CurrencyCode": { "type": ["null", "string"] @@ -63,18 +64,6 @@ "KeywordStatus": { "type": ["null", "string"] }, - "HistoricalExpectedCtr": { - "type": ["null", "number"] - }, - "HistoricalAdRelevance": { - "type": ["null", "number"] - }, - "HistoricalLandingPageExperience": { - "type": ["null", "number"] - }, - "HistoricalQualityScore": { - "type": ["null", "number"] - }, "Impressions": { "type": ["null", "integer"] }, diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report_daily.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report_daily.json new file mode 100644 index 0000000000000..48e35d9f3ce9d --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report_daily.json @@ -0,0 +1,190 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "KeywordId": { + "type": ["null", "integer"] + }, + "Keyword": { + "type": ["null", "string"] + }, + "AdId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date" + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Language": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "KeywordStatus": { + "type": ["null", "string"] + }, + "HistoricalExpectedCtr": { + "type": ["null", "number"] + }, + "HistoricalAdRelevance": { + "type": ["null", "number"] + }, + "HistoricalLandingPageExperience": { + "type": ["null", "number"] + }, + "HistoricalQualityScore": { + "type": ["null", "number"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "CurrentMaxCpc": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "QualityScore": { + "type": ["null", "number"] + }, + "ExpectedCtr": { + "type": ["null", "string"] + }, + "AdRelevance": { + "type": ["null", "number"] + }, + "LandingPageExperience": { + "type": ["null", "number"] + }, + "QualityImpact": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "CustomParameters": { + "type": ["null", "string"] + }, + "FinalAppUrl": { + "type": ["null", "string"] + }, + "Mainline1Bid": { + "type": ["null", "number"] + }, + "MainlineBid": { + "type": ["null", "number"] + }, + "FirstPageBid": { + "type": ["null", "number"] + }, + "FinalUrlSuffix": { + "type": ["null", "string"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "ViewThroughConversionsQualified": { + "type": ["null", "number"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report_hourly.json new file mode 100644 index 0000000000000..831c389d24a1d --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keyword_performance_report_hourly.json @@ -0,0 +1,180 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "additionalProperties": true, + "properties": { + "AccountId": { + "type": ["null", "integer"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "KeywordId": { + "type": ["null", "integer"] + }, + "Keyword": { + "type": ["null", "string"] + }, + "AdId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "Language": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "AccountName": { + "type": ["null", "string"] + }, + "CampaignName": { + "type": ["null", "string"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "KeywordStatus": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "CurrentMaxCpc": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "QualityScore": { + "type": ["null", "number"] + }, + "ExpectedCtr": { + "type": ["null", "string"] + }, + "AdRelevance": { + "type": ["null", "number"] + }, + "LandingPageExperience": { + "type": ["null", "number"] + }, + "QualityImpact": { + "type": ["null", "number"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "CustomParameters": { + "type": ["null", "string"] + }, + "FinalAppUrl": { + "type": ["null", "string"] + }, + "Mainline1Bid": { + "type": ["null", "number"] + }, + "MainlineBid": { + "type": ["null", "number"] + }, + "FirstPageBid": { + "type": ["null", "number"] + }, + "FinalUrlSuffix": { + "type": ["null", "string"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "ViewThroughConversionsQualified": { + "type": ["null", "number"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "Conversions": { + "type": ["null", "number"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keywords.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keywords.json index 9987fe828be26..4f25c1378753e 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keywords.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/keywords.json @@ -30,7 +30,9 @@ "type": ["null", "string"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Editorial Appeal Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/labels.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/labels.json index a23858825318b..40845b0eb0354 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/labels.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/labels.json @@ -21,7 +21,9 @@ "type": ["null", "string"] }, "Modified Time": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" }, "Status": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report.json index ca9dcb71d9c61..57c50442e3955 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report.json @@ -12,7 +12,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CampaignName": { "type": ["null", "string"] @@ -81,7 +82,7 @@ "type": ["null", "number"] }, "CostPerConversion": { - "type": ["null", "integer"] + "type": ["null", "number"] }, "Language": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report_hourly.json new file mode 100644 index 0000000000000..27e35b2fc6707 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/search_query_performance_report_hourly.json @@ -0,0 +1,182 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountName": { + "type": ["null", "string"] + }, + "AccountNumber": { + "type": ["null", "string"] + }, + "AccountId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CampaignName": { + "type": ["null", "string"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "AdId": { + "type": ["null", "integer"] + }, + "AdType": { + "type": ["null", "string"] + }, + "DestinationUrl": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "CampaignStatus": { + "type": ["null", "string"] + }, + "AdStatus": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "SearchQuery": { + "type": ["null", "string"] + }, + "Keyword": { + "type": ["null", "string"] + }, + "AdGroupCriterionId": { + "type": ["null", "string"] + }, + "Conversions": { + "type": ["null", "integer"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "Language": { + "type": ["null", "string"] + }, + "KeywordId": { + "type": ["null", "integer"] + }, + "Network": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + }, + "AccountStatus": { + "type": ["null", "string"] + }, + "AdGroupStatus": { + "type": ["null", "string"] + }, + "KeywordStatus": { + "type": ["null", "string"] + }, + "CampaignType": { + "type": ["null", "string"] + }, + "CustomerId": { + "type": ["null", "integer"] + }, + "CustomerName": { + "type": ["null", "string"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "Goal": { + "type": ["null", "string"] + }, + "GoalType": { + "type": ["null", "string"] + }, + "AbsoluteTopImpressionRatePercent": { + "type": ["null", "number"] + }, + "TopImpressionRatePercent": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AllConversionsQualified": { + "type": ["null", "number"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report.json index acee84403de12..8edbd095b6055 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report.json +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report.json @@ -12,7 +12,8 @@ "type": ["null", "integer"] }, "TimePeriod": { - "type": ["null", "string"] + "type": ["null", "string"], + "format": "date" }, "CampaignName": { "type": ["null", "string"] diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report_hourly.json b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report_hourly.json new file mode 100644 index 0000000000000..1bd42e6b8087d --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/schemas/user_location_performance_report_hourly.json @@ -0,0 +1,215 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "AccountName": { + "type": ["null", "string"] + }, + "AccountNumber": { + "type": ["null", "string"] + }, + "AccountId": { + "type": ["null", "integer"] + }, + "TimePeriod": { + "type": ["null", "string"], + "format": "date-time", + "airbyte_type": "timestamp_with_timezone" + }, + "CampaignName": { + "type": ["null", "string"] + }, + "CampaignId": { + "type": ["null", "integer"] + }, + "AdGroupName": { + "type": ["null", "string"] + }, + "AdGroupId": { + "type": ["null", "integer"] + }, + "Country": { + "type": ["null", "string"] + }, + "State": { + "type": ["null", "string"] + }, + "MetroArea": { + "type": ["null", "string"] + }, + "CurrencyCode": { + "type": ["null", "string"] + }, + "AdDistribution": { + "type": ["null", "string"] + }, + "Impressions": { + "type": ["null", "integer"] + }, + "Clicks": { + "type": ["null", "integer"] + }, + "Ctr": { + "type": ["null", "number"] + }, + "AverageCpc": { + "type": ["null", "number"] + }, + "Spend": { + "type": ["null", "number"] + }, + "AveragePosition": { + "type": ["null", "number"] + }, + "ProximityTargetLocation": { + "type": ["null", "string"] + }, + "Radius": { + "type": ["null", "integer"] + }, + "Language": { + "type": ["null", "string"] + }, + "City": { + "type": ["null", "string"] + }, + "QueryIntentCountry": { + "type": ["null", "string"] + }, + "QueryIntentState": { + "type": ["null", "string"] + }, + "QueryIntentCity": { + "type": ["null", "string"] + }, + "QueryIntentDMA": { + "type": ["null", "string"] + }, + "BidMatchType": { + "type": ["null", "string"] + }, + "DeliveredMatchType": { + "type": ["null", "string"] + }, + "Network": { + "type": ["null", "string"] + }, + "TopVsOther": { + "type": ["null", "string"] + }, + "DeviceType": { + "type": ["null", "string"] + }, + "DeviceOS": { + "type": ["null", "string"] + }, + "Assists": { + "type": ["null", "integer"] + }, + "Conversions": { + "type": ["null", "integer"] + }, + "ConversionRate": { + "type": ["null", "number"] + }, + "Revenue": { + "type": ["null", "number"] + }, + "ReturnOnAdSpend": { + "type": ["null", "number"] + }, + "CostPerConversion": { + "type": ["null", "number"] + }, + "CostPerAssist": { + "type": ["null", "number"] + }, + "RevenuePerConversion": { + "type": ["null", "number"] + }, + "RevenuePerAssist": { + "type": ["null", "number"] + }, + "County": { + "type": ["null", "string"] + }, + "PostalCode": { + "type": ["null", "string"] + }, + "QueryIntentCounty": { + "type": ["null", "string"] + }, + "QueryIntentPostalCode": { + "type": ["null", "string"] + }, + "LocationId": { + "type": ["null", "integer"] + }, + "QueryIntentLocationId": { + "type": ["null", "integer"] + }, + "AllConversions": { + "type": ["null", "integer"] + }, + "AllRevenue": { + "type": ["null", "number"] + }, + "AllConversionRate": { + "type": ["null", "number"] + }, + "AllCostPerConversion": { + "type": ["null", "number"] + }, + "AllReturnOnAdSpend": { + "type": ["null", "number"] + }, + "AllRevenuePerConversion": { + "type": ["null", "number"] + }, + "ViewThroughConversions": { + "type": ["null", "integer"] + }, + "Goal": { + "type": ["null", "string"] + }, + "GoalType": { + "type": ["null", "string"] + }, + "AbsoluteTopImpressionRatePercent": { + "type": ["null", "number"] + }, + "TopImpressionRatePercent": { + "type": ["null", "number"] + }, + "AverageCpm": { + "type": ["null", "number"] + }, + "ConversionsQualified": { + "type": ["null", "number"] + }, + "AllConversionsQualified": { + "type": ["null", "number"] + }, + "ViewThroughConversionsQualified": { + "type": ["null", "number"] + }, + "Neighborhood": { + "type": ["null", "string"] + }, + "QueryIntentNeighborhood": { + "type": ["null", "string"] + }, + "ViewThroughRevenue": { + "type": ["null", "number"] + }, + "CampaignType": { + "type": ["null", "string"] + }, + "AssetGroupId": { + "type": ["null", "integer"] + }, + "AssetGroupName": { + "type": ["null", "string"] + } + } +} diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/source.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/source.py index 9ff7a581479c3..c18fcf5a913f1 100644 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/source.py +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/source.py @@ -9,8 +9,10 @@ from airbyte_cdk.sources import AbstractSource from airbyte_cdk.sources.streams import Stream from airbyte_cdk.utils import AirbyteTracedException +from source_bing_ads.base_streams import Accounts, AdGroups, Ads, Campaigns +from source_bing_ads.bulk_streams import AdGroupLabels, AppInstallAdLabels, AppInstallAds, CampaignLabels, KeywordLabels, Keywords, Labels from source_bing_ads.client import Client -from source_bing_ads.streams import ( # noqa: F401 +from source_bing_ads.report_streams import ( # noqa: F401 AccountImpressionPerformanceReportDaily, AccountImpressionPerformanceReportHourly, AccountImpressionPerformanceReportMonthly, @@ -19,52 +21,41 @@ AccountPerformanceReportHourly, AccountPerformanceReportMonthly, AccountPerformanceReportWeekly, - Accounts, AdGroupImpressionPerformanceReportDaily, AdGroupImpressionPerformanceReportHourly, AdGroupImpressionPerformanceReportMonthly, AdGroupImpressionPerformanceReportWeekly, - AdGroupLabels, AdGroupPerformanceReportDaily, AdGroupPerformanceReportHourly, AdGroupPerformanceReportMonthly, AdGroupPerformanceReportWeekly, - AdGroups, AdPerformanceReportDaily, AdPerformanceReportHourly, AdPerformanceReportMonthly, AdPerformanceReportWeekly, - Ads, AgeGenderAudienceReportDaily, AgeGenderAudienceReportHourly, AgeGenderAudienceReportMonthly, AgeGenderAudienceReportWeekly, - AppInstallAdLabels, - AppInstallAds, BingAdsReportingServiceStream, BudgetSummaryReport, CampaignImpressionPerformanceReportDaily, CampaignImpressionPerformanceReportHourly, CampaignImpressionPerformanceReportMonthly, CampaignImpressionPerformanceReportWeekly, - CampaignLabels, CampaignPerformanceReportDaily, CampaignPerformanceReportHourly, CampaignPerformanceReportMonthly, CampaignPerformanceReportWeekly, - Campaigns, CustomReport, GeographicPerformanceReportDaily, GeographicPerformanceReportHourly, GeographicPerformanceReportMonthly, GeographicPerformanceReportWeekly, - KeywordLabels, KeywordPerformanceReportDaily, KeywordPerformanceReportHourly, KeywordPerformanceReportMonthly, KeywordPerformanceReportWeekly, - Keywords, - Labels, SearchQueryPerformanceReportDaily, SearchQueryPerformanceReportHourly, SearchQueryPerformanceReportMonthly, diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/streams.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/streams.py deleted file mode 100644 index ecbb4cb450f89..0000000000000 --- a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/streams.py +++ /dev/null @@ -1,1358 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# -import os -import re -import ssl -import time -import xml.etree.ElementTree as ET -from abc import ABC, abstractmethod -from datetime import timezone -from typing import Any, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Union -from urllib.error import URLError -from urllib.parse import urlparse - -import _csv -import pandas as pd -import pendulum -from airbyte_cdk.models import SyncMode -from airbyte_cdk.sources.streams import IncrementalMixin, Stream -from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer -from bingads.service_client import ServiceClient -from bingads.v13.internal.reporting.row_report import _RowReport -from bingads.v13.internal.reporting.row_report_iterator import _RowReportRecord -from bingads.v13.reporting.reporting_service_manager import ReportingServiceManager -from numpy import nan -from source_bing_ads.client import Client -from source_bing_ads.reports import ( - ALL_CONVERSION_FIELDS, - ALL_REVENUE_FIELDS, - AVERAGE_FIELDS, - BUDGET_FIELDS, - CONVERSION_FIELDS, - HISTORICAL_FIELDS, - LOW_QUALITY_FIELDS, - REVENUE_FIELDS, - PerformanceReportsMixin, - ReportsMixin, -) -from suds import WebFault, sudsobject - - -class BingAdsBaseStream(Stream, ABC): - primary_key: Optional[Union[str, List[str], List[List[str]]]] = None - - def __init__(self, client: Client, config: Mapping[str, Any]) -> None: - super().__init__() - self.client = client - self.config = config - - -class BingAdsStream(BingAdsBaseStream, ABC): - @property - @abstractmethod - def operation_name(self) -> str: - """ - Specifies operation name to use for a current stream - """ - pass - - @property - @abstractmethod - def service_name(self) -> str: - """ - Specifies bing ads service name for a current stream - """ - pass - - @property - def parent_key_to_foreign_key_map(self) -> MutableMapping[str, str]: - """ - Specifies dict with field in record as kay and slice key as value to be inserted in record in transform method. - """ - return {} - - def transform(self, record: MutableMapping[str, Any], stream_slice: Mapping[str, Any], **kwargs) -> MutableMapping[str, Any]: - foreign_keys = {key: stream_slice.get(value) for key, value in self.parent_key_to_foreign_key_map.items()} - return record | foreign_keys - - @property - def _service(self) -> Union[ServiceClient, ReportingServiceManager]: - return self.client.get_service(service_name=self.service_name) - - @property - def _user_id(self) -> int: - return self._get_user_id() - - # TODO remove once Microsoft support confirm their SSL certificates are always valid... - def _get_user_id(self, number_of_retries=10): - """""" - try: - return self._service.GetUser().User.Id - except URLError as error: - if isinstance(error.reason, ssl.SSLError): - self.logger.warning("SSL certificate error, retrying...") - if number_of_retries > 0: - time.sleep(1) - return self._get_user_id(number_of_retries - 1) - else: - raise error - - def next_page_token(self, response: sudsobject.Object, **kwargs: Mapping[str, Any]) -> Optional[Mapping[str, Any]]: - """ - Default method for streams that don't support pagination - """ - return None - - def send_request(self, params: Mapping[str, Any], customer_id: str, account_id: str = None) -> Mapping[str, Any]: - request_kwargs = { - "service_name": self.service_name, - "customer_id": customer_id, - "account_id": account_id, - "operation_name": self.operation_name, - "params": params, - } - request = self.client.request(**request_kwargs) - return request - - def read_records( - self, - sync_mode: SyncMode, - stream_slice: Mapping[str, Any] = None, - stream_state: Mapping[str, Any] = None, - **kwargs: Mapping[str, Any], - ) -> Iterable[Mapping[str, Any]]: - stream_state = stream_state or {} - next_page_token = None - account_id = str(stream_slice.get("account_id")) if stream_slice else None - customer_id = str(stream_slice.get("customer_id")) if stream_slice else None - - while True: - params = self.request_params( - stream_state=stream_state, - stream_slice=stream_slice, - next_page_token=next_page_token, - account_id=account_id, - ) - response = self.send_request(params, customer_id=customer_id, account_id=account_id) - for record in self.parse_response(response): - yield self.transform(record, stream_slice) - - next_page_token = self.next_page_token(response, current_page_token=next_page_token) - if not next_page_token: - break - - yield from [] - - def parse_response(self, response: sudsobject.Object, **kwargs) -> Iterable[Mapping]: - if response is not None and hasattr(response, self.data_field): - yield from self.client.asdict(response)[self.data_field] - - yield from [] - - -class BingAdsCampaignManagementStream(BingAdsStream, ABC): - service_name: str = "CampaignManagement" - - @property - @abstractmethod - def data_field(self) -> str: - """ - Specifies root object name in a stream response - """ - pass - - @property - @abstractmethod - def additional_fields(self) -> Optional[str]: - """ - Specifies which additional fields to fetch for a current stream. - Expected format: field names separated by space - """ - pass - - def parse_response(self, response: sudsobject.Object, **kwargs) -> Iterable[Mapping]: - if response is not None and hasattr(response, self.data_field): - yield from self.client.asdict(response)[self.data_field] - - yield from [] - - -class BingAdsReportingServiceStream(BingAdsStream, ABC): - - cursor_field = "TimePeriod" - service_name: str = "ReportingService" - operation_name: str = "download_report" - - def parse_response(self, response: sudsobject.Object, **kwargs: Mapping[str, Any]) -> Iterable[Mapping]: - if response is not None: - try: - for row in response.report_records: - yield {column: self.get_column_value(row, column) for column in self.report_columns} - except _csv.Error as e: - self.logger.warning(f"CSV report file for stream `{self.name}` is broken or cannot be read correctly: {e}, skipping ...") - - yield from [] - - def get_column_value(self, row: _RowReportRecord, column: str) -> Union[str, None, int, float]: - """ - Reads field value from row and transforms: - 1. empty values to logical None - 2. Percent values to numeric string e.g. "12.25%" -> "12.25" - """ - value = row.value(column) - if not value or value == "--": - return None - if "%" in value: - value = value.replace("%", "") - if value and set(self.get_json_schema()["properties"].get(column, {}).get("type")) & {"integer", "number"}: - value = value.replace(",", "") - return value - - -class BingAdsBulkStream(BingAdsBaseStream, IncrementalMixin, ABC): - - transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) - cursor_field = "Modified Time" - _state = {} - - @property - @abstractmethod - def data_scope(self) -> List[str]: - """ - Defines scopes or types of data to download. Docs: https://learn.microsoft.com/en-us/advertising/bulk-service/datascope?view=bingads-13 - """ - - @property - @abstractmethod - def download_entities(self) -> List[str]: - """ - Defines the entities that should be downloaded. Docs: https://learn.microsoft.com/en-us/advertising/bulk-service/downloadentity?view=bingads-13 - """ - - def stream_slices( - self, - **kwargs: Mapping[str, Any], - ) -> Iterable[Optional[Mapping[str, Any]]]: - for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): - yield {"account_id": account["Id"], "customer_id": account["ParentCustomerId"]} - - yield from [] - - @property - def state(self) -> Mapping[str, Any]: - return self._state - - @state.setter - def state(self, value: Mapping[str, Any]): - self._state.update({str(value["Account Id"]): {self.cursor_field: value[self.cursor_field]}}) - - def get_start_date(self, stream_state: Mapping[str, Any] = None, account_id: str = None): - """ - Start_date in request can be provided only if it is sooner than 30 days from now - """ - min_available_date = pendulum.now().subtract(days=30).astimezone(tz=timezone.utc) - start_date = self.client.reports_start_date - if stream_state.get(account_id, {}).get(self.cursor_field): - start_date = pendulum.from_format(stream_state[account_id][self.cursor_field], "MM/DD/YYYY HH:mm:ss.SSS") - return None if start_date < min_available_date else min_available_date - - def read_records( - self, - sync_mode: SyncMode, - stream_slice: Mapping[str, Any] = None, - stream_state: Mapping[str, Any] = None, - **kwargs: Mapping[str, Any], - ) -> Iterable[Mapping[str, Any]]: - stream_state = stream_state or {} - account_id = str(stream_slice.get("account_id")) if stream_slice else None - customer_id = str(stream_slice.get("customer_id")) if stream_slice else None - - report_file_path = self.client.get_bulk_entity( - data_scope=self.data_scope, - download_entities=self.download_entities, - customer_id=customer_id, - account_id=account_id, - start_date=self.get_start_date(stream_state, account_id), - ) - for record in self.read_with_chunks(report_file_path): - record = self.transform(record, stream_slice) - yield record - self.state = record - - yield from [] - - def read_with_chunks(self, path: str, chunk_size: int = 1024) -> Iterable[Tuple[int, Mapping[str, Any]]]: - try: - with open(path, "r") as data: - chunks = pd.read_csv(data, chunksize=chunk_size, iterator=True, dialect="unix", dtype=object) - for chunk in chunks: - chunk = chunk.replace({nan: None}).to_dict(orient="records") - for row in chunk: - if row.get("Type") not in ("Format Version", "Account"): - yield row - except pd.errors.EmptyDataError as e: - self.logger.info(f"Empty data received. {e}") - yield from [] - except IOError as ioe: - self.logger.fatal( - f"The IO/Error occurred while reading tmp data. Called: {path}. Stream: {self.name}", - ) - raise ioe - finally: - # remove binary tmp file, after data is read - os.remove(path) - - def transform(self, record: MutableMapping[str, Any], stream_slice: Mapping[str, Any], **kwargs) -> MutableMapping[str, Any]: - """ - Bing Ads Bulk API returns all available properties for all entities. - This method filter out only available properties. - """ - actual_record = {key: value for key, value in record.items() if key in self.get_json_schema()["properties"].keys()} - actual_record["Account Id"] = stream_slice.get("account_id") - return actual_record - - -class AppInstallAds(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/app-install-ad?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["AppInstallAds"] - - primary_key = "Id" - - -class AppInstallAdLabels(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/app-install-ad-label?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["AppInstallAdLabels"] - - primary_key = "Id" - - -class Labels(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/label?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["Labels"] - - primary_key = "Id" - - -class KeywordLabels(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/keyword-label?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["KeywordLabels"] - - primary_key = "Id" - - -class Keywords(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/keyword?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["Keywords"] - - primary_key = "Id" - - -class CampaignLabels(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/campaign-label?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["CampaignLabels"] - - primary_key = "Id" - - -class AdGroupLabels(BingAdsBulkStream): - """ - https://learn.microsoft.com/en-us/advertising/bulk-service/ad-group-label?view=bingads-13 - """ - - data_scope = ["EntityData"] - download_entities = ["AdGroupLabels"] - - primary_key = "Id" - - -class Accounts(BingAdsStream): - """ - Searches for accounts that the current authenticated user can access. - API doc: https://docs.microsoft.com/en-us/advertising/customer-management-service/searchaccounts?view=bingads-13 - Account schema: https://docs.microsoft.com/en-us/advertising/customer-management-service/advertiseraccount?view=bingads-13 - Stream caches incoming responses to be able to reuse this data in Campaigns stream - """ - - primary_key = "Id" - # Stream caches incoming responses to avoid duplicated http requests - use_cache: bool = True - data_field: str = "AdvertiserAccount" - service_name: str = "CustomerManagementService" - operation_name: str = "SearchAccounts" - additional_fields: str = "TaxCertificate AccountMode" - # maximum page size - page_size_limit: int = 1000 - - def next_page_token(self, response: sudsobject.Object, current_page_token: Optional[int]) -> Optional[Mapping[str, Any]]: - current_page_token = current_page_token or 0 - if response is not None and hasattr(response, self.data_field): - return None if self.page_size_limit > len(response[self.data_field]) else current_page_token + 1 - else: - return None - - def request_params( - self, - next_page_token: Mapping[str, Any] = None, - **kwargs: Mapping[str, Any], - ) -> MutableMapping[str, Any]: - predicates = { - "Predicate": [ - { - "Field": "UserId", - "Operator": "Equals", - "Value": self._user_id, - } - ] - } - - paging = self._service.factory.create("ns5:Paging") - paging.Index = next_page_token or 0 - paging.Size = self.page_size_limit - return { - "PageInfo": paging, - "Predicates": predicates, - "ReturnAdditionalFields": self.additional_fields, - } - - -class Campaigns(BingAdsCampaignManagementStream): - """ - Gets the campaigns for all provided accounts. - API doc: https://docs.microsoft.com/en-us/advertising/campaign-management-service/getcampaignsbyaccountid?view=bingads-13 - Campaign schema: https://docs.microsoft.com/en-us/advertising/campaign-management-service/campaign?view=bingads-13 - Stream caches incoming responses to be able to reuse this data in AdGroups stream - """ - - primary_key = "Id" - # Stream caches incoming responses to avoid duplicated http requests - use_cache: bool = True - data_field: str = "Campaign" - operation_name: str = "GetCampaignsByAccountId" - additional_fields: Iterable[str] = [ - "AdScheduleUseSearcherTimeZone", - "BidStrategyId", - "CpvCpmBiddingScheme", - "DynamicDescriptionSetting", - "DynamicFeedSetting", - "MaxConversionValueBiddingScheme", - "MultimediaAdsBidAdjustment", - "TargetImpressionShareBiddingScheme", - "TargetSetting", - "VerifiedTrackingSetting", - ] - campaign_types: Iterable[str] = ["Audience", "DynamicSearchAds", "Search", "Shopping"] - - parent_key_to_foreign_key_map = { - "AccountId": "account_id", - "CustomerId": "customer_id", - } - - def request_params( - self, - stream_slice: Mapping[str, Any] = None, - **kwargs: Mapping[str, Any], - ) -> MutableMapping[str, Any]: - return { - "AccountId": stream_slice["account_id"], - "CampaignType": " ".join(self.campaign_types), - "ReturnAdditionalFields": " ".join(self.additional_fields), - } - - def stream_slices( - self, - **kwargs: Mapping[str, Any], - ) -> Iterable[Optional[Mapping[str, Any]]]: - for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): - yield {"account_id": account["Id"], "customer_id": account["ParentCustomerId"]} - - yield from [] - - -class AdGroups(BingAdsCampaignManagementStream): - """ - Gets the ad groups for all provided accounts. - API doc: https://docs.microsoft.com/en-us/advertising/campaign-management-service/getadgroupsbycampaignid?view=bingads-13 - AdGroup schema: https://docs.microsoft.com/en-us/advertising/campaign-management-service/adgroup?view=bingads-13 - Stream caches incoming responses to be able to reuse this data in Ads stream - """ - - primary_key = "Id" - # Stream caches incoming responses to avoid duplicated http requests - use_cache: bool = True - data_field: str = "AdGroup" - operation_name: str = "GetAdGroupsByCampaignId" - additional_fields: str = "AdGroupType AdScheduleUseSearcherTimeZone CpmBid CpvBid MultimediaAdsBidAdjustment" - - parent_key_to_foreign_key_map = {"CampaignId": "campaign_id", "AccountId": "account_id", "CustomerId": "customer_id"} - - def request_params( - self, - stream_slice: Mapping[str, Any] = None, - **kwargs: Mapping[str, Any], - ) -> MutableMapping[str, Any]: - return {"CampaignId": stream_slice["campaign_id"], "ReturnAdditionalFields": self.additional_fields} - - def stream_slices( - self, - **kwargs: Mapping[str, Any], - ) -> Iterable[Optional[Mapping[str, Any]]]: - campaigns = Campaigns(self.client, self.config) - for account in Accounts(self.client, self.config).read_records(SyncMode.full_refresh): - for campaign in campaigns.read_records( - sync_mode=SyncMode.full_refresh, stream_slice={"account_id": account["Id"], "customer_id": account["ParentCustomerId"]} - ): - yield {"campaign_id": campaign["Id"], "account_id": account["Id"], "customer_id": account["ParentCustomerId"]} - - yield from [] - - -class Ads(BingAdsCampaignManagementStream): - """ - Retrieves the ads for all provided accounts. - API doc: https://docs.microsoft.com/en-us/advertising/campaign-management-service/getadsbyadgroupid?view=bingads-13 - Ad schema: https://docs.microsoft.com/en-us/advertising/campaign-management-service/ad?view=bingads-13 - """ - - primary_key = "Id" - data_field: str = "Ad" - operation_name: str = "GetAdsByAdGroupId" - additional_fields: str = "ImpressionTrackingUrls Videos LongHeadlines" - ad_types: Iterable[str] = [ - "Text", - "Image", - "Product", - "AppInstall", - "ExpandedText", - "DynamicSearch", - "ResponsiveAd", - "ResponsiveSearch", - ] - - parent_key_to_foreign_key_map = {"AdGroupId": "ad_group_id", "AccountId": "account_id", "CustomerId": "customer_id"} - - def request_params( - self, - stream_slice: Mapping[str, Any] = None, - **kwargs: Mapping[str, Any], - ) -> MutableMapping[str, Any]: - return { - "AdGroupId": stream_slice["ad_group_id"], - "AdTypes": {"AdType": self.ad_types}, - "ReturnAdditionalFields": self.additional_fields, - } - - def stream_slices( - self, - **kwargs: Mapping[str, Any], - ) -> Iterable[Optional[Mapping[str, Any]]]: - ad_groups = AdGroups(self.client, self.config) - for slice in ad_groups.stream_slices(sync_mode=SyncMode.full_refresh): - for ad_group in ad_groups.read_records(sync_mode=SyncMode.full_refresh, stream_slice=slice): - yield {"ad_group_id": ad_group["Id"], "account_id": slice["account_id"], "customer_id": slice["customer_id"]} - yield from [] - - -class BudgetSummaryReport(ReportsMixin, BingAdsReportingServiceStream): - report_name: str = "BudgetSummaryReport" - report_aggregation = None - cursor_field = "Date" - report_schema_name = "budget_summary_report" - primary_key = "Date" - - report_columns = [ - "AccountName", - "AccountNumber", - "AccountId", - "CampaignName", - "CampaignId", - "Date", - "MonthlyBudget", - "DailySpend", - "MonthToDateSpend", - ] - - -class CampaignPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream): - report_name: str = "CampaignPerformanceReport" - - report_schema_name = "campaign_performance_report" - primary_key = [ - "AccountId", - "CampaignId", - "TimePeriod", - "CurrencyCode", - "AdDistribution", - "DeviceType", - "Network", - "DeliveredMatchType", - "DeviceOS", - "TopVsOther", - "BidMatchType", - ] - - report_columns = [ - *primary_key, - "AccountName", - "CampaignName", - "CampaignType", - "CampaignStatus", - "CampaignLabels", - "Impressions", - "Clicks", - "Ctr", - "Spend", - "CostPerConversion", - "QualityScore", - "AdRelevance", - "LandingPageExperience", - "PhoneImpressions", - "PhoneCalls", - "Ptr", - "Assists", - "ReturnOnAdSpend", - "CostPerAssist", - "CustomParameters", - "ViewThroughConversions", - "AllCostPerConversion", - "AllReturnOnAdSpend", - *ALL_CONVERSION_FIELDS, - *ALL_REVENUE_FIELDS, - *AVERAGE_FIELDS, - *CONVERSION_FIELDS, - *LOW_QUALITY_FIELDS, - *REVENUE_FIELDS, - *BUDGET_FIELDS, - ] - - -class CampaignPerformanceReportHourly(CampaignPerformanceReport): - report_aggregation = "Hourly" - - -class CampaignPerformanceReportDaily(CampaignPerformanceReport): - report_aggregation = "Daily" - report_columns = [ - *CampaignPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class CampaignPerformanceReportWeekly(CampaignPerformanceReport): - report_aggregation = "Weekly" - report_columns = [ - *CampaignPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class CampaignPerformanceReportMonthly(CampaignPerformanceReport): - report_aggregation = "Monthly" - report_columns = [ - *CampaignPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class CampaignImpressionPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - """ - https://learn.microsoft.com/en-us/advertising/reporting-service/adgroupperformancereportrequest?view=bingads-13 - Primary key cannot be set: due to included `Impression Share Performance Statistics` some fields should be removed, - see https://learn.microsoft.com/en-us/advertising/guides/reports?view=bingads-13#columnrestrictions for more info. - """ - - report_name: str = "CampaignPerformanceReport" - - report_schema_name = "campaign_impression_performance_report" - - primary_key = None - - @property - def report_columns(self) -> Iterable[str]: - return list(self.get_json_schema().get("properties", {}).keys()) - - -class CampaignImpressionPerformanceReportHourly(CampaignImpressionPerformanceReport): - report_aggregation = "Hourly" - - report_schema_name = "campaign_impression_performance_report_hourly" - - -class CampaignImpressionPerformanceReportDaily(CampaignImpressionPerformanceReport): - report_aggregation = "Daily" - - -class CampaignImpressionPerformanceReportWeekly(CampaignImpressionPerformanceReport): - report_aggregation = "Weekly" - - -class CampaignImpressionPerformanceReportMonthly(CampaignImpressionPerformanceReport): - report_aggregation = "Monthly" - - -class AdPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream): - report_name: str = "AdPerformanceReport" - - report_schema_name = "ad_performance_report" - primary_key = [ - "AccountId", - "CampaignId", - "AdGroupId", - "AdId", - "TimePeriod", - "CurrencyCode", - "AdDistribution", - "DeviceType", - "Language", - "Network", - "DeviceOS", - "TopVsOther", - "BidMatchType", - "DeliveredMatchType", - ] - - report_columns = [ - *primary_key, - "AccountName", - "CampaignName", - "CampaignType", - "AdGroupName", - "Impressions", - "Clicks", - "Ctr", - "Spend", - "CostPerConversion", - "DestinationUrl", - "Assists", - "ReturnOnAdSpend", - "CostPerAssist", - "CustomParameters", - "FinalAppUrl", - "AdDescription", - "AdDescription2", - "ViewThroughConversions", - "ViewThroughConversionsQualified", - "AllCostPerConversion", - "AllReturnOnAdSpend", - *CONVERSION_FIELDS, - *AVERAGE_FIELDS, - *ALL_CONVERSION_FIELDS, - *ALL_REVENUE_FIELDS, - *REVENUE_FIELDS, - ] - - -class AdPerformanceReportHourly(AdPerformanceReport): - report_aggregation = "Hourly" - - -class AdPerformanceReportDaily(AdPerformanceReport): - report_aggregation = "Daily" - - -class AdPerformanceReportWeekly(AdPerformanceReport): - report_aggregation = "Weekly" - - -class AdPerformanceReportMonthly(AdPerformanceReport): - report_aggregation = "Monthly" - - -class AdGroupPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - report_name: str = "AdGroupPerformanceReport" - - report_schema_name = "ad_group_performance_report" - - primary_key = [ - "AccountId", - "CampaignId", - "AdGroupId", - "TimePeriod", - "CurrencyCode", - "AdDistribution", - "DeviceType", - "Network", - "DeliveredMatchType", - "DeviceOS", - "TopVsOther", - "BidMatchType", - "Language", - ] - - report_columns = [ - *primary_key, - "AccountName", - "CampaignName", - "CampaignType", - "AdGroupName", - "AdGroupType", - "Impressions", - "Clicks", - "Ctr", - "Spend", - "CostPerConversion", - "QualityScore", - "ExpectedCtr", - "AdRelevance", - "LandingPageExperience", - "PhoneImpressions", - "PhoneCalls", - "Ptr", - "Assists", - "CostPerAssist", - "CustomParameters", - "FinalUrlSuffix", - "ViewThroughConversions", - "AllCostPerConversion", - "AllReturnOnAdSpend", - *ALL_CONVERSION_FIELDS, - *ALL_REVENUE_FIELDS, - *AVERAGE_FIELDS, - *CONVERSION_FIELDS, - *REVENUE_FIELDS, - ] - - -class AdGroupPerformanceReportHourly(AdGroupPerformanceReport): - report_aggregation = "Hourly" - - -class AdGroupPerformanceReportDaily(AdGroupPerformanceReport): - report_aggregation = "Daily" - report_columns = [ - *AdGroupPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class AdGroupPerformanceReportWeekly(AdGroupPerformanceReport): - report_aggregation = "Weekly" - report_columns = [ - *AdGroupPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class AdGroupPerformanceReportMonthly(AdGroupPerformanceReport): - report_aggregation = "Monthly" - report_columns = [ - *AdGroupPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class AdGroupImpressionPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - """ - https://learn.microsoft.com/en-us/advertising/reporting-service/adgroupperformancereportrequest?view=bingads-13 - Primary key cannot be set: due to included `Impression Share Performance Statistics` some fields should be removed, - see https://learn.microsoft.com/en-us/advertising/guides/reports?view=bingads-13#columnrestrictions for more info. - """ - - report_name: str = "AdGroupPerformanceReport" - - report_schema_name = "ad_group_impression_performance_report" - - @property - def report_columns(self) -> Iterable[str]: - return list(self.get_json_schema().get("properties", {}).keys()) - - -class AdGroupImpressionPerformanceReportHourly(AdGroupImpressionPerformanceReport): - report_aggregation = "Hourly" - report_schema_name = "ad_group_impression_performance_report_hourly" - - -class AdGroupImpressionPerformanceReportDaily(AdGroupImpressionPerformanceReport): - report_aggregation = "Daily" - - -class AdGroupImpressionPerformanceReportWeekly(AdGroupImpressionPerformanceReport): - report_aggregation = "Weekly" - - -class AdGroupImpressionPerformanceReportMonthly(AdGroupImpressionPerformanceReport): - report_aggregation = "Monthly" - - -class KeywordPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - - report_name: str = "KeywordPerformanceReport" - - report_schema_name = "keyword_performance_report" - primary_key = [ - "AccountId", - "CampaignId", - "AdGroupId", - "KeywordId", - "AdId", - "TimePeriod", - "CurrencyCode", - "DeliveredMatchType", - "AdDistribution", - "DeviceType", - "Language", - "Network", - "DeviceOS", - "TopVsOther", - "BidMatchType", - ] - - report_columns = [ - *primary_key, - "AccountName", - "CampaignName", - "AdGroupName", - "Keyword", - "KeywordStatus", - "Impressions", - "Clicks", - "Ctr", - "CurrentMaxCpc", - "Spend", - "CostPerConversion", - "QualityScore", - "ExpectedCtr", - "AdRelevance", - "LandingPageExperience", - "QualityImpact", - "Assists", - "ReturnOnAdSpend", - "CostPerAssist", - "CustomParameters", - "FinalAppUrl", - "Mainline1Bid", - "MainlineBid", - "FirstPageBid", - "FinalUrlSuffix", - "ViewThroughConversions", - "ViewThroughConversionsQualified", - "AllCostPerConversion", - "AllReturnOnAdSpend", - *CONVERSION_FIELDS, - *AVERAGE_FIELDS, - *ALL_CONVERSION_FIELDS, - *ALL_REVENUE_FIELDS, - *REVENUE_FIELDS, - ] - - -class KeywordPerformanceReportHourly(KeywordPerformanceReport): - report_aggregation = "Hourly" - - -class KeywordPerformanceReportDaily(KeywordPerformanceReport): - report_aggregation = "Daily" - report_columns = [ - *KeywordPerformanceReport.report_columns, - *HISTORICAL_FIELDS, - ] - - -class KeywordPerformanceReportWeekly(KeywordPerformanceReport): - report_aggregation = "Weekly" - - -class KeywordPerformanceReportMonthly(KeywordPerformanceReport): - report_aggregation = "Monthly" - - -class GeographicPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - - report_name: str = "GeographicPerformanceReport" - - report_schema_name = "geographic_performance_report" - - # Need to override the primary key here because the one inherited from the PerformanceReportsMixin - # is incorrect for the geographic performance reports - primary_key = None - - report_columns = [ - "AccountId", - "CampaignId", - "AdGroupId", - "TimePeriod", - "Country", - "CurrencyCode", - "DeliveredMatchType", - "AdDistribution", - "DeviceType", - "Language", - "Network", - "DeviceOS", - "TopVsOther", - "BidMatchType", - "MetroArea", - "State", - "City", - "AdGroupName", - "Ctr", - "ProximityTargetLocation", - "Radius", - "Assists", - "ReturnOnAdSpend", - "CostPerAssist", - "LocationType", - "MostSpecificLocation", - "AccountStatus", - "CampaignStatus", - "AdGroupStatus", - "County", - "PostalCode", - "LocationId", - "BaseCampaignId", - "AllCostPerConversion", - "AllReturnOnAdSpend", - "ViewThroughConversions", - "Goal", - "GoalType", - "AbsoluteTopImpressionRatePercent", - "TopImpressionRatePercent", - "AllConversionsQualified", - "ViewThroughConversionsQualified", - "Neighborhood", - "ViewThroughRevenue", - "CampaignType", - "AssetGroupId", - "AssetGroupName", - "AssetGroupStatus", - "Clicks", - "Spend", - "Impressions", - "CostPerConversion", - "AccountName", - "AccountNumber", - "CampaignName", - *CONVERSION_FIELDS, - *AVERAGE_FIELDS, - *ALL_CONVERSION_FIELDS, - *ALL_REVENUE_FIELDS, - *REVENUE_FIELDS, - ] - - -class GeographicPerformanceReportHourly(GeographicPerformanceReport): - report_aggregation = "Hourly" - - -class GeographicPerformanceReportDaily(GeographicPerformanceReport): - report_aggregation = "Daily" - - -class GeographicPerformanceReportWeekly(GeographicPerformanceReport): - report_aggregation = "Weekly" - - -class GeographicPerformanceReportMonthly(GeographicPerformanceReport): - report_aggregation = "Monthly" - - -class AccountPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream): - - report_name: str = "AccountPerformanceReport" - - report_schema_name = "account_performance_report" - primary_key = [ - "AccountId", - "TimePeriod", - "CurrencyCode", - "AdDistribution", - "DeviceType", - "Network", - "DeliveredMatchType", - "DeviceOS", - "TopVsOther", - "BidMatchType", - ] - - report_columns = [ - *primary_key, - "AccountName", - "AccountNumber", - "PhoneImpressions", - "PhoneCalls", - "Clicks", - "Ctr", - "Spend", - "Impressions", - "CostPerConversion", - "Ptr", - "Assists", - "ReturnOnAdSpend", - "CostPerAssist", - *AVERAGE_FIELDS, - *CONVERSION_FIELDS, - *LOW_QUALITY_FIELDS, - *REVENUE_FIELDS, - ] - - -class AccountPerformanceReportHourly(AccountPerformanceReport): - report_aggregation = "Hourly" - - -class AccountPerformanceReportDaily(AccountPerformanceReport): - report_aggregation = "Daily" - - -class AccountPerformanceReportWeekly(AccountPerformanceReport): - report_aggregation = "Weekly" - - -class AccountPerformanceReportMonthly(AccountPerformanceReport): - report_aggregation = "Monthly" - - -class AccountImpressionPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - """ - Report source: https://docs.microsoft.com/en-us/advertising/reporting-service/accountperformancereportrequest?view=bingads-13 - Primary key cannot be set: due to included `Impression Share Performance Statistics` some fields should be removed, - see https://learn.microsoft.com/en-us/advertising/guides/reports?view=bingads-13#columnrestrictions for more info. - """ - - report_name: str = "AccountPerformanceReport" - report_schema_name = "account_impression_performance_report" - primary_key = None - - @property - def report_columns(self): - return list(self.get_json_schema().get("properties", {}).keys()) - - -class AccountImpressionPerformanceReportHourly(AccountImpressionPerformanceReport): - report_aggregation = "Hourly" - - report_schema_name = "account_impression_performance_report_hourly" - - -class AccountImpressionPerformanceReportDaily(AccountImpressionPerformanceReport): - report_aggregation = "Daily" - - -class AccountImpressionPerformanceReportWeekly(AccountImpressionPerformanceReport): - report_aggregation = "Weekly" - - -class AccountImpressionPerformanceReportMonthly(AccountImpressionPerformanceReport): - report_aggregation = "Monthly" - - -class AgeGenderAudienceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - - report_name: str = "AgeGenderAudienceReport" - - report_schema_name = "age_gender_audience_report" - primary_key = ["AgeGroup", "Gender", "TimePeriod", "AccountId", "CampaignId", "Language", "AdDistribution"] - - @property - def report_columns(self): - return list(self.get_json_schema().get("properties", {}).keys()) - - -class AgeGenderAudienceReportHourly(AgeGenderAudienceReport): - report_aggregation = "Hourly" - - -class AgeGenderAudienceReportDaily(AgeGenderAudienceReport): - report_aggregation = "Daily" - - -class AgeGenderAudienceReportWeekly(AgeGenderAudienceReport): - report_aggregation = "Weekly" - - -class AgeGenderAudienceReportMonthly(AgeGenderAudienceReport): - report_aggregation = "Monthly" - - -class SearchQueryPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - - report_name: str = "SearchQueryPerformanceReport" - - report_schema_name = "search_query_performance_report" - primary_key = [ - "SearchQuery", - "Keyword", - "TimePeriod", - "AccountId", - "CampaignId", - "Language", - "DeliveredMatchType", - "DeviceType", - "DeviceOS", - "TopVsOther", - ] - - @property - def report_columns(self) -> List[str]: - return list(self.get_json_schema().get("properties", {}).keys()) - - -class SearchQueryPerformanceReportHourly(SearchQueryPerformanceReport): - report_aggregation = "Hourly" - - -class SearchQueryPerformanceReportDaily(SearchQueryPerformanceReport): - report_aggregation = "Daily" - - -class SearchQueryPerformanceReportWeekly(SearchQueryPerformanceReport): - report_aggregation = "Weekly" - - -class SearchQueryPerformanceReportMonthly(SearchQueryPerformanceReport): - report_aggregation = "Monthly" - - -class UserLocationPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - - report_name: str = "UserLocationPerformanceReport" - report_schema_name = "user_location_performance_report" - primary_key = [ - "AccountId", - "AdGroupId", - "CampaignId", - "DeliveredMatchType", - "DeviceOS", - "DeviceType", - "Language", - "LocationId", - "QueryIntentLocationId", - "TimePeriod", - "TopVsOther", - ] - - @property - def report_columns(self) -> List[str]: - return list(self.get_json_schema().get("properties", {}).keys()) - - -class UserLocationPerformanceReportHourly(UserLocationPerformanceReport): - report_aggregation = "Hourly" - - -class UserLocationPerformanceReportDaily(UserLocationPerformanceReport): - report_aggregation = "Daily" - - -class UserLocationPerformanceReportWeekly(UserLocationPerformanceReport): - report_aggregation = "Weekly" - - -class UserLocationPerformanceReportMonthly(UserLocationPerformanceReport): - report_aggregation = "Monthly" - - -class CustomReport(PerformanceReportsMixin, BingAdsReportingServiceStream, ABC): - transformer: TypeTransformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) - custom_report_columns = [] - report_schema_name = None - primary_key = None - - @property - def cursor_field(self) -> Union[str, List[str]]: - # Summary aggregation doesn't include TimePeriod field - if self.report_aggregation != "Summary": - return "TimePeriod" - - @property - def report_columns(self): - # adding common and default columns - if "AccountId" not in self.custom_report_columns: - self.custom_report_columns.append("AccountId") - if self.cursor_field and self.cursor_field not in self.custom_report_columns: - self.custom_report_columns.append(self.cursor_field) - return list(frozenset(self.custom_report_columns)) - - def get_json_schema(self) -> Mapping[str, Any]: - columns_schema = {col: {"type": ["null", "string"]} for col in self.report_columns} - schema: Mapping[str, Any] = { - "$schema": "https://json-schema.org/draft-07/schema#", - "type": ["null", "object"], - "additionalProperties": True, - "properties": columns_schema, - } - return schema - - def validate_report_configuration(self) -> Tuple[bool, str]: - # gets /bingads/v13/proxies/production/reporting_service.xml - reporting_service_file = self.client.get_service(self.service_name)._get_service_info_dict(self.client.api_version)[ - ("reporting", self.client.environment) - ] - tree = ET.parse(urlparse(reporting_service_file).path) - request_object = tree.find(f".//{{*}}complexType[@name='{self.report_name}Request']") - - report_object_columns = self._get_object_columns(request_object, tree) - is_custom_cols_in_report_object_cols = all(x in report_object_columns for x in self.custom_report_columns) - - if not is_custom_cols_in_report_object_cols: - return False, ( - f"Reporting Columns are invalid. Columns that you provided don't belong to Reporting Data Object Columns:" - f" {self.custom_report_columns}. Please ensure it is correct in Bing Ads Docs." - ) - - return True, "" - - def _clear_namespace(self, type: str) -> str: - return re.sub(r"^[a-z]+:", "", type) - - def _get_object_columns(self, request_el: ET.Element, tree: ET.ElementTree) -> List[str]: - column_el = request_el.find(".//{*}element[@name='Columns']") - array_of_columns_name = self._clear_namespace(column_el.get("type")) - - array_of_columns_elements = tree.find(f".//{{*}}complexType[@name='{array_of_columns_name}']") - inner_array_of_columns_elements = array_of_columns_elements.find(".//{*}element") - column_el_name = self._clear_namespace(inner_array_of_columns_elements.get("type")) - - column_el = tree.find(f".//{{*}}simpleType[@name='{column_el_name}']") - column_enum_items = column_el.findall(".//{*}enumeration") - column_enum_items_values = [el.get("value") for el in column_enum_items] - return column_enum_items_values - - def get_report_record_timestamp(self, datestring: str) -> int: - """ - Parse report date field based on aggregation type - """ - if not self.report_aggregation: - date = pendulum.from_format(datestring, "M/D/YYYY") - else: - if self.report_aggregation in ["DayOfWeek", "HourOfDay"]: - return int(datestring) - if self.report_aggregation == "Hourly": - date = pendulum.from_format(datestring, "YYYY-MM-DD|H") - else: - date = pendulum.parse(datestring) - - return date.int_timestamp - - def send_request(self, params: Mapping[str, Any], customer_id: str, account_id: str) -> _RowReport: - try: - return super().send_request(params, customer_id, account_id) - except WebFault as e: - self.logger.error( - f"Could not sync custom report {self.name}: Please validate your column and aggregation configuration. " - f"Error form server: [{e.fault.faultstring}]" - ) diff --git a/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/utils.py b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/utils.py new file mode 100644 index 0000000000000..785b1237452af --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/source_bing_ads/utils.py @@ -0,0 +1,29 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +from datetime import datetime, timezone + + +def transform_bulk_datetime_format_to_rfc_3339(original_value: str) -> str: + """ + Bing Ads Bulk API provides datetime fields in custom format with milliseconds: "04/27/2023 18:00:14.970" + Return datetime in RFC3339 format: "2023-04-27T18:00:14.970+00:00" + """ + return datetime.strptime(original_value, "%m/%d/%Y %H:%M:%S.%f").replace(tzinfo=timezone.utc).isoformat(timespec="milliseconds") + + +def transform_date_format_to_rfc_3339(original_value: str) -> str: + """ + Bing Ads API provides date fields in custom format: "04/27/2023" + Return date in RFC3339 format: "2023-04-27" + """ + return datetime.strptime(original_value, "%m/%d/%Y").replace(tzinfo=timezone.utc).strftime("%Y-%m-%d") + + +def transform_report_hourly_datetime_format_to_rfc_3339(original_value: str) -> str: + """ + Bing Ads API reports with hourly aggregation provides date fields in custom format: "2023-11-04|11" + Return date in RFC3339 format: "2023-11-04T11:00:00+00:00" + """ + return datetime.strptime(original_value, "%Y-%m-%d|%H").replace(tzinfo=timezone.utc).isoformat(timespec="seconds") diff --git a/airbyte-integrations/connectors/source-bing-ads/unit_tests/conftest.py b/airbyte-integrations/connectors/source-bing-ads/unit_tests/conftest.py new file mode 100644 index 0000000000000..fec097978ffff --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/unit_tests/conftest.py @@ -0,0 +1,62 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# +from unittest.mock import patch + +import pytest + + +@pytest.fixture(name="config") +def config_fixture(): + """Generates streams settings from a config file""" + return { + "tenant_id": "common", + "developer_token": "fake_developer_token", + "refresh_token": "fake_refresh_token", + "client_id": "fake_client_id", + "reports_start_date": "2020-01-01", + "lookback_window": 0, + } + + +@pytest.fixture(name="config_with_custom_reports") +def config_with_custom_reports_fixture(): + """Generates streams settings with custom reports from a config file""" + return { + "tenant_id": "common", + "developer_token": "fake_developer_token", + "refresh_token": "fake_refresh_token", + "client_id": "fake_client_id", + "reports_start_date": "2020-01-01", + "lookback_window": 0, + "custom_reports": [ + { + "name": "my test custom report", + "reporting_object": "DSAAutoTargetPerformanceReport", + "report_columns": [ + "AbsoluteTopImpressionRatePercent", + "AccountId", + "AccountName", + "AccountNumber", + "AccountStatus", + "AdDistribution", + "AdGroupId", + "AdGroupName", + "AdGroupStatus", + "AdId", + "AllConversionRate", + "AllConversions", + "AllConversionsQualified", + "AllCostPerConversion", + "AllReturnOnAdSpend", + "AllRevenue", + ], + "report_aggregation": "Weekly", + } + ], + } + + +@pytest.fixture(name="logger_mock") +def logger_mock_fixture(): + return patch("source_bing_ads.source.AirbyteLogger") diff --git a/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_bulk_streams.py b/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_bulk_streams.py new file mode 100644 index 0000000000000..23f7efb7bb8e9 --- /dev/null +++ b/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_bulk_streams.py @@ -0,0 +1,112 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +from pathlib import Path +from unittest.mock import patch + +import pendulum +import pytest +import source_bing_ads +from freezegun import freeze_time +from pendulum import UTC, DateTime +from source_bing_ads.base_streams import Accounts +from source_bing_ads.bulk_streams import AppInstallAds + + +@patch.object(source_bing_ads.source, "Client") +def test_bulk_stream_stream_slices(mocked_client, config): + slices = AppInstallAds(mocked_client, config).stream_slices() + assert list(slices) == [] + + app_install_ads = AppInstallAds(mocked_client, config) + accounts_read_records = iter([{"Id": 180519267, "ParentCustomerId": 100}, {"Id": 180278106, "ParentCustomerId": 200}]) + with patch.object(Accounts, "read_records", return_value=accounts_read_records): + slices = app_install_ads.stream_slices() + assert list(slices) == [{"account_id": 180519267, "customer_id": 100}, {"account_id": 180278106, "customer_id": 200}] + + +@patch.object(source_bing_ads.source, "Client") +def test_bulk_stream_transform(mocked_client, config): + record = {"Ad Group": "Ad Group", "App Id": "App Id", "Campaign": "Campaign", "Custom Parameter": "Custom Parameter"} + transformed_record = AppInstallAds(mocked_client, config).transform( + record=record, stream_slice={"account_id": 180519267, "customer_id": 100} + ) + assert transformed_record == { + "Account Id": 180519267, + "Ad Group": "Ad Group", + "App Id": "App Id", + "Campaign": "Campaign", + "Custom Parameter": "Custom Parameter", + } + + +@patch.object(source_bing_ads.source, "Client") +def test_bulk_stream_read_with_chunks(mocked_client, config): + path_to_file = Path(__file__).parent / "app_install_ads.csv" + path_to_file_base = Path(__file__).parent / "app_install_ads_base.csv" + with open(path_to_file_base, "r") as f1, open(path_to_file, "a") as f2: + for line in f1: + f2.write(line) + + app_install_ads = AppInstallAds(mocked_client, config) + result = app_install_ads.read_with_chunks(path=path_to_file) + assert next(result) == { + "Ad Group": "AdGroupNameGoesHere", + "App Id": "AppStoreIdGoesHere", + "App Platform": "Android", + "Campaign": "ParentCampaignNameGoesHere", + "Client Id": "ClientIdGoesHere", + "Custom Parameter": "{_promoCode}=PROMO1; {_season}=summer", + "Destination Url": None, + "Device Preference": "All", + "Display Url": None, + "Final Url": "FinalUrlGoesHere", + "Final Url Suffix": None, + "Id": None, + "Mobile Final Url": None, + "Modified Time": None, + "Name": None, + "Parent Id": "-1111", + "Promotion": None, + "Status": "Active", + "Text": "Find New Customers & Increase Sales!", + "Title": "Contoso Quick Setup", + "Tracking Template": None, + "Type": "App Install Ad", + } + + +@patch.object(source_bing_ads.source, "Client") +@freeze_time("2023-11-01T12:00:00.000+00:00") +@pytest.mark.parametrize( + "stream_state, config_start_date, expected_start_date", + [ + ({"some_account_id": {"Modified Time": "2023-10-15T12:00:00.000+00:00"}}, "2020-01-01", DateTime(2023, 10, 15, 12, 0, 0, tzinfo=UTC)), + ({"another_account_id": {"Modified Time": "2023-10-15T12:00:00.000+00:00"}}, "2020-01-01", None), + ({}, "2020-01-01", None), + ({}, "2023-10-21", DateTime(2023, 10, 21, 0, 0, 0, tzinfo=UTC)), + ], + ids=["state_within_30_days", "state_within_30_days_another_account_id", "empty_state", "empty_state_start_date_within_30"] +) +def test_bulk_stream_start_date(mocked_client, config, stream_state, config_start_date, expected_start_date): + mocked_client.reports_start_date = pendulum.parse(config_start_date) if config_start_date else None + stream = AppInstallAds(mocked_client, config) + assert expected_start_date == stream.get_start_date(stream_state, 'some_account_id') + + +@patch.object(source_bing_ads.source, "Client") +def test_bulk_stream_stream_state(mocked_client, config): + stream = AppInstallAds(mocked_client, config) + stream.state = {"Account Id": "some_account_id", "Modified Time": "04/27/2023 18:00:14.970"} + assert stream.state == {"some_account_id": {"Modified Time": "2023-04-27T18:00:14.970+00:00"}} + stream.state = {"Account Id": "some_account_id", "Modified Time": "05/27/2023 18:00:14.970"} + assert stream.state == {"some_account_id": {"Modified Time": "2023-05-27T18:00:14.970+00:00"}} + stream.state = {"Account Id": "some_account_id", "Modified Time": "05/25/2023 18:00:14.970"} + assert stream.state == {"some_account_id": {"Modified Time": "2023-05-27T18:00:14.970+00:00"}} + + +@patch.object(source_bing_ads.source, "Client") +def test_bulk_stream_custom_transform_date_rfc3339(mocked_client, config): + stream = AppInstallAds(mocked_client, config) + assert "2023-04-27T18:00:14.970+00:00" == stream.custom_transform_date_rfc3339("04/27/2023 18:00:14.970", stream.get_json_schema()["properties"][stream.cursor_field]) diff --git a/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_reports.py b/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_reports.py index 3c7ecc8534300..44c5329bd83a2 100644 --- a/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_reports.py +++ b/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_reports.py @@ -3,27 +3,65 @@ # import copy -from unittest.mock import Mock +import xml.etree.ElementTree as ET +from unittest.mock import MagicMock, Mock, patch +from urllib.parse import urlparse +import _csv import pendulum import pytest +import source_bing_ads +from bingads.service_info import SERVICE_INFO_DICT_V13 from bingads.v13.internal.reporting.row_report_iterator import _RowReportRecord, _RowValues -from source_bing_ads.reports import PerformanceReportsMixin, ReportsMixin -from source_bing_ads.source import SourceBingAds -from source_bing_ads.streams import ( +from source_bing_ads.base_streams import Accounts +from source_bing_ads.report_streams import ( + AccountImpressionPerformanceReportDaily, + AccountImpressionPerformanceReportHourly, + AccountPerformanceReportDaily, + AccountPerformanceReportHourly, + AccountPerformanceReportMonthly, + AdGroupImpressionPerformanceReportDaily, + AdGroupImpressionPerformanceReportHourly, + AdGroupPerformanceReportDaily, + AdGroupPerformanceReportHourly, + AdPerformanceReportDaily, + AdPerformanceReportHourly, + AgeGenderAudienceReportDaily, + AgeGenderAudienceReportHourly, + BingAdsReportingServicePerformanceStream, BingAdsReportingServiceStream, + BudgetSummaryReport, + CampaignImpressionPerformanceReportDaily, + CampaignImpressionPerformanceReportHourly, + CampaignPerformanceReportDaily, + CampaignPerformanceReportHourly, GeographicPerformanceReportDaily, GeographicPerformanceReportHourly, GeographicPerformanceReportMonthly, GeographicPerformanceReportWeekly, + KeywordPerformanceReportDaily, + KeywordPerformanceReportHourly, + SearchQueryPerformanceReportDaily, + SearchQueryPerformanceReportHourly, + UserLocationPerformanceReportDaily, + UserLocationPerformanceReportHourly, ) +from source_bing_ads.source import SourceBingAds +from suds import WebFault + +TEST_CONFIG = { + "developer_token": "developer_token", + "client_id": "client_id", + "refresh_token": "refresh_token", + "reports_start_date": "2020-01-01T00:00:00Z", +} class TestClient: pass -class TestReport(ReportsMixin, BingAdsReportingServiceStream, SourceBingAds): +class TestReport(BingAdsReportingServiceStream, SourceBingAds): date_format, report_columns, report_name, cursor_field = "YYYY-MM-DD", None, None, "Time" report_aggregation = "Monthly" report_schema_name = "campaign_performance_report" @@ -32,7 +70,7 @@ def __init__(self) -> None: self.client = TestClient() -class TestPerformanceReport(PerformanceReportsMixin, BingAdsReportingServiceStream, SourceBingAds): +class TestPerformanceReport(BingAdsReportingServicePerformanceStream, SourceBingAds): date_format, report_columns, report_name, cursor_field = "YYYY-MM-DD", None, None, "Time" report_aggregation = "Monthly" report_schema_name = "campaign_performance_report" @@ -65,25 +103,38 @@ def test_get_column_value(): assert test_report.get_column_value(record, "Assists") == "123456789" +@patch.object(source_bing_ads.source, "Client") +def test_AccountPerformanceReportMonthly_request_params(mocked_client, config): + accountperformancereportmonthly = AccountPerformanceReportMonthly(mocked_client, config) + request_params = accountperformancereportmonthly.request_params(account_id=180278106, stream_slice={"time_period": "ThisYear"}) + del request_params["report_request"] + assert request_params == { + "overwrite_result_file": True, + "result_file_directory": "/tmp", + "result_file_name": "AccountPerformanceReport", + "timeout_in_milliseconds": 300000, + } + + def test_get_updated_state_init_state(): test_report = TestReport() stream_state = {} latest_record = {"AccountId": 123, "Time": "2020-01-02"} new_state = test_report.get_updated_state(stream_state, latest_record) - assert new_state["123"]["Time"] == (pendulum.parse("2020-01-02")).timestamp() + assert new_state["123"]["Time"] == "2020-01-02" def test_get_updated_state_new_state(): test_report = TestReport() - stream_state = {"123": {"Time": pendulum.parse("2020-01-01").timestamp()}} + stream_state = {"123": {"Time": "2020-01-01"}} latest_record = {"AccountId": 123, "Time": "2020-01-02"} new_state = test_report.get_updated_state(stream_state, latest_record) - assert new_state["123"]["Time"] == pendulum.parse("2020-01-02").timestamp() + assert new_state["123"]["Time"] == "2020-01-02" def test_get_updated_state_state_unchanged(): test_report = TestReport() - stream_state = {"123": {"Time": pendulum.parse("2020-01-03").timestamp()}} + stream_state = {"123": {"Time": "2020-01-03"}} latest_record = {"AccountId": 123, "Time": "2020-01-02"} new_state = test_report.get_updated_state(copy.deepcopy(stream_state), latest_record) assert stream_state == new_state @@ -91,29 +142,60 @@ def test_get_updated_state_state_unchanged(): def test_get_updated_state_state_new_account(): test_report = TestReport() - stream_state = {"123": {"Time": pendulum.parse("2020-01-03").timestamp()}} + stream_state = {"123": {"Time": "2020-01-03"}} latest_record = {"AccountId": 234, "Time": "2020-01-02"} new_state = test_report.get_updated_state(stream_state, latest_record) assert "234" in new_state and "123" in new_state - assert new_state["234"]["Time"] == pendulum.parse("2020-01-02").timestamp() + assert new_state["234"]["Time"] == "2020-01-02" -def test_get_report_record_timestamp_daily(): - test_report = TestReport() - test_report.report_aggregation = "Daily" - assert pendulum.parse("2020-01-01").timestamp() == test_report.get_report_record_timestamp("2020-01-01") +@pytest.mark.parametrize( + "stream_report_daily_cls", + ( + AccountImpressionPerformanceReportDaily, + AccountPerformanceReportDaily, + AdGroupImpressionPerformanceReportDaily, + AdGroupPerformanceReportDaily, + AgeGenderAudienceReportDaily, + AdPerformanceReportDaily, + CampaignImpressionPerformanceReportDaily, + CampaignPerformanceReportDaily, + KeywordPerformanceReportDaily, + SearchQueryPerformanceReportDaily, + UserLocationPerformanceReportDaily, + GeographicPerformanceReportDaily, + ), +) +def test_get_report_record_timestamp_daily(stream_report_daily_cls): + stream_report = stream_report_daily_cls(client=Mock(), config=TEST_CONFIG) + assert "2020-01-01" == stream_report.get_report_record_timestamp("2020-01-01") def test_get_report_record_timestamp_without_aggregation(): - test_report = TestReport() - test_report.report_aggregation = None - assert pendulum.parse("2020-07-20").timestamp() == test_report.get_report_record_timestamp("7/20/2020") + stream_report = BudgetSummaryReport(client=Mock(), config=TEST_CONFIG) + assert "2020-07-20" == stream_report.get_report_record_timestamp("7/20/2020") -def test_get_report_record_timestamp_hourly(): - test_report = TestReport() - test_report.report_aggregation = "Hourly" - assert pendulum.parse("2020-01-01T15:00:00").timestamp() == test_report.get_report_record_timestamp("2020-01-01|15") +@pytest.mark.parametrize( + "stream_report_hourly_cls", + ( + AccountImpressionPerformanceReportHourly, + AccountPerformanceReportHourly, + AdGroupImpressionPerformanceReportHourly, + AdGroupPerformanceReportHourly, + AgeGenderAudienceReportHourly, + AdPerformanceReportHourly, + CampaignImpressionPerformanceReportHourly, + CampaignPerformanceReportHourly, + KeywordPerformanceReportHourly, + SearchQueryPerformanceReportHourly, + UserLocationPerformanceReportHourly, + GeographicPerformanceReportHourly, + ), +) +def test_get_report_record_timestamp_hourly(stream_report_hourly_cls): + stream_report = stream_report_hourly_cls(client=Mock(), config=TEST_CONFIG) + assert "2020-01-01T15:00:00+00:00" == stream_report.get_report_record_timestamp("2020-01-01|15") def test_report_get_start_date_wo_stream_state(): @@ -130,7 +212,7 @@ def test_report_get_start_date_with_stream_state(): test_report = TestReport() test_report.cursor_field = "cursor_field" test_report.client.reports_start_date = "2020-01-01" - stream_state = {"123": {"cursor_field": 1681766997}} + stream_state = {"123": {"cursor_field": "2023-04-17T21:29:57+00:00"}} account_id = "123" assert expected_start_date == test_report.get_start_date(stream_state, account_id) @@ -140,7 +222,7 @@ def test_report_get_start_date_performance_report_with_stream_state(): test_report = TestPerformanceReport() test_report.cursor_field = "cursor_field" test_report.config = {"lookback_window": 10} - stream_state = {"123": {"cursor_field": 1681766997}} + stream_state = {"123": {"cursor_field": "2023-04-17T21:29:57+00:00"}} account_id = "123" assert expected_start_date == test_report.get_start_date(stream_state, account_id) @@ -167,11 +249,181 @@ def test_report_get_start_date_performance_report_wo_stream_state(): ), ) def test_geographic_performance_report_pk(performance_report_cls): - config = { - "developer_token": "developer_token", - "client_id": "client_id", - "refresh_token": "refresh_token", - "reports_start_date": "2020-01-01T00:00:00Z", - } - stream = performance_report_cls(client=Mock(), config=config) + stream = performance_report_cls(client=Mock(), config=TEST_CONFIG) assert stream.primary_key is None + + +def test_report_parse_response_csv_error(caplog): + stream_report = AccountPerformanceReportHourly(client=Mock(), config=TEST_CONFIG) + fake_response = MagicMock() + fake_response.report_records.__iter__ = MagicMock(side_effect=_csv.Error) + list(stream_report.parse_response(fake_response)) + assert "CSV report file for stream `account_performance_report_hourly` is broken or cannot be read correctly: , skipping ..." in caplog.messages + + +@patch.object(source_bing_ads.source, "Client") +def test_custom_report_clear_namespace(mocked_client, config_with_custom_reports, logger_mock): + custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] + assert custom_report._clear_namespace("tns:ReportAggregation") == "ReportAggregation" + + +@patch.object(source_bing_ads.source, "Client") +def test_custom_report_get_object_columns(mocked_client, config_with_custom_reports, logger_mock): + reporting_service_mock = MagicMock() + reporting_service_mock._get_service_info_dict.return_value = SERVICE_INFO_DICT_V13 + mocked_client.get_service.return_value = reporting_service_mock + mocked_client.environment = "production" + + custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] + + tree = ET.parse(urlparse(SERVICE_INFO_DICT_V13[("reporting", mocked_client.environment)]).path) + request_object = tree.find(f".//{{*}}complexType[@name='{custom_report.report_name}Request']") + + assert custom_report._get_object_columns(request_object, tree) == [ + "TimePeriod", + "AccountId", + "AccountName", + "AccountNumber", + "AccountStatus", + "CampaignId", + "CampaignName", + "CampaignStatus", + "AdGroupId", + "AdGroupName", + "AdGroupStatus", + "AdDistribution", + "Language", + "Network", + "TopVsOther", + "DeviceType", + "DeviceOS", + "BidStrategyType", + "TrackingTemplate", + "CustomParameters", + "DynamicAdTargetId", + "DynamicAdTarget", + "DynamicAdTargetStatus", + "WebsiteCoverage", + "Impressions", + "Clicks", + "Ctr", + "AverageCpc", + "Spend", + "AveragePosition", + "Conversions", + "ConversionRate", + "CostPerConversion", + "Assists", + "Revenue", + "ReturnOnAdSpend", + "CostPerAssist", + "RevenuePerConversion", + "RevenuePerAssist", + "AllConversions", + "AllRevenue", + "AllConversionRate", + "AllCostPerConversion", + "AllReturnOnAdSpend", + "AllRevenuePerConversion", + "ViewThroughConversions", + "Goal", + "GoalType", + "AbsoluteTopImpressionRatePercent", + "TopImpressionRatePercent", + "AverageCpm", + "ConversionsQualified", + "AllConversionsQualified", + "ViewThroughConversionsQualified", + "AdId", + "ViewThroughRevenue", + ] + + +@patch.object(source_bing_ads.source, "Client") +def test_custom_report_send_request(mocked_client, config_with_custom_reports, logger_mock, caplog): + class Fault: + faultstring = "Invalid Client Data" + + custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] + with patch.object(BingAdsReportingServiceStream, "send_request", side_effect=WebFault(fault=Fault(), document=None)): + custom_report.send_request(params={}, customer_id="13131313", account_id="800800808") + assert ( + "Could not sync custom report my test custom report: Please validate your column and aggregation configuration. " + "Error form server: [Invalid Client Data]" + ) in caplog.text + + +@pytest.mark.parametrize( + "aggregation, datastring, expected", + [ + ( + "Summary", + "11/13/2023", + "2023-11-13", + ), + ( + "Hourly", + "2022-11-13|10", + "2022-11-13T10:00:00+00:00", + ), + ( + "Daily", + "2022-11-13", + "2022-11-13", + ), + ( + "Weekly", + "2022-11-13", + "2022-11-13", + ), + ( + "Monthly", + "2022-11-13", + "2022-11-13", + ), + ( + "WeeklyStartingMonday", + "2022-11-13", + "2022-11-13", + ), + ], +) +@patch.object(source_bing_ads.source, "Client") +def test_custom_report_get_report_record_timestamp(mocked_client, config_with_custom_reports, aggregation, datastring, expected): + custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] + custom_report.report_aggregation = aggregation + assert custom_report.get_report_record_timestamp(datastring) == expected + + +@patch.object(source_bing_ads.source, "Client") +def test_account_performance_report_monthly_stream_slices(mocked_client, config): + account_performance_report_monthly = AccountPerformanceReportMonthly(mocked_client, config) + accounts_read_records = iter([{"Id": 180519267, "ParentCustomerId": 100}, {"Id": 180278106, "ParentCustomerId": 200}]) + with patch.object(Accounts, "read_records", return_value=accounts_read_records): + stream_slice = list(account_performance_report_monthly.stream_slices()) + assert stream_slice == [ + {'account_id': 180519267, 'customer_id': 100, 'time_period': 'LastYear'}, + {'account_id': 180519267, 'customer_id': 100, 'time_period': 'ThisYear'}, + {'account_id': 180278106, 'customer_id': 200, 'time_period': 'LastYear'}, + {'account_id': 180278106, 'customer_id': 200, 'time_period': 'ThisYear'} + ] + + +@pytest.mark.parametrize( + "aggregation", + [ + "DayOfWeek", + "HourOfDay", + ], +) +@patch.object(source_bing_ads.source, "Client") +def test_custom_performance_report_no_last_year_stream_slices(mocked_client, config_with_custom_reports, aggregation): + custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] + custom_report.report_aggregation = aggregation + accounts_read_records = iter([{"Id": 180519267, "ParentCustomerId": 100}, {"Id": 180278106, "ParentCustomerId": 200}]) + with patch.object(Accounts, "read_records", return_value=accounts_read_records): + stream_slice = list(custom_report.stream_slices()) + assert stream_slice == [ + {"account_id": 180519267, "customer_id": 100, "time_period": "ThisYear"}, + {"account_id": 180278106, "customer_id": 200, "time_period": "ThisYear"}, + ] diff --git a/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_source.py b/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_source.py index 379eb5afacb1e..7d89ff112b1aa 100644 --- a/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_source.py +++ b/airbyte-integrations/connectors/source-bing-ads/unit_tests/test_source.py @@ -2,76 +2,15 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # -import xml.etree.ElementTree as ET -from pathlib import Path from unittest.mock import MagicMock, patch -from urllib.parse import urlparse import pytest import source_bing_ads from airbyte_cdk.models import SyncMode from airbyte_cdk.utils import AirbyteTracedException from bingads.service_info import SERVICE_INFO_DICT_V13 -from source_bing_ads.reports import ReportsMixin +from source_bing_ads.base_streams import Accounts, AdGroups, Ads, Campaigns from source_bing_ads.source import SourceBingAds -from source_bing_ads.streams import AccountPerformanceReportMonthly, Accounts, AdGroups, Ads, AppInstallAds, Campaigns -from suds import WebFault - - -@pytest.fixture(name="config") -def config_fixture(): - """Generates streams settings from a config file""" - return { - "tenant_id": "common", - "developer_token": "fake_developer_token", - "refresh_token": "fake_refresh_token", - "client_id": "fake_client_id", - "reports_start_date": "2020-01-01", - "lookback_window": 0, - } - - -@pytest.fixture(name="config_with_custom_reports") -def config_with_custom_reports_fixture(): - """Generates streams settings with custom reports from a config file""" - return { - "tenant_id": "common", - "developer_token": "fake_developer_token", - "refresh_token": "fake_refresh_token", - "client_id": "fake_client_id", - "reports_start_date": "2020-01-01", - "lookback_window": 0, - "custom_reports": [ - { - "name": "my test custom report", - "reporting_object": "DSAAutoTargetPerformanceReport", - "report_columns": [ - "AbsoluteTopImpressionRatePercent", - "AccountId", - "AccountName", - "AccountNumber", - "AccountStatus", - "AdDistribution", - "AdGroupId", - "AdGroupName", - "AdGroupStatus", - "AdId", - "AllConversionRate", - "AllConversions", - "AllConversionsQualified", - "AllCostPerConversion", - "AllReturnOnAdSpend", - "AllRevenue", - ], - "report_aggregation": "Weekly", - } - ], - } - - -@pytest.fixture(name="logger_mock") -def logger_mock_fixture(): - return patch("source_bing_ads.source.AirbyteLogger") @patch.object(source_bing_ads.source, "Client") @@ -234,36 +173,6 @@ def test_ads_stream_slices(mocked_client, config): ] -@patch.object(source_bing_ads.source, "Client") -def test_AccountPerformanceReportMonthly_request_params(mocked_client, config): - - accountperformancereportmonthly = AccountPerformanceReportMonthly(mocked_client, config) - request_params = accountperformancereportmonthly.request_params(account_id=180278106, stream_slice={"time_period": "ThisYear"}) - del request_params["report_request"] - assert request_params == { - "overwrite_result_file": True, - # 'report_request': , - "result_file_directory": "/tmp", - "result_file_name": "AccountPerformanceReport", - "timeout_in_milliseconds": 300000, - } - - -@patch.object(source_bing_ads.source, "Client") -def test_AccountPerformanceReportMonthly_stream_slices(mocked_client, config): - - accountperformancereportmonthly = AccountPerformanceReportMonthly(mocked_client, config) - accounts_read_records = iter([{"Id": 180519267, "ParentCustomerId": 100}, {"Id": 180278106, "ParentCustomerId": 200}]) - with patch.object(Accounts, "read_records", return_value=accounts_read_records): - stream_slice = list(accountperformancereportmonthly.stream_slices()) - assert stream_slice == [ - {"account_id": 180519267, "customer_id": 100, "time_period": "LastYear"}, - {"account_id": 180519267, "customer_id": 100, "time_period": "ThisYear"}, - {"account_id": 180278106, "customer_id": 200, "time_period": "LastYear"}, - {"account_id": 180278106, "customer_id": 200, "time_period": "ThisYear"}, - ] - - @pytest.mark.parametrize( ("stream", "stream_slice"), ( @@ -280,69 +189,6 @@ def test_streams_full_refresh(mocked_client, config, stream, stream_slice): mocked_client.request.assert_called_once() -@patch.object(source_bing_ads.source, "Client") -def test_bulk_stream_stream_slices(mocked_client, config): - slices = AppInstallAds(mocked_client, config).stream_slices() - assert list(slices) == [] - - app_install_ads = AppInstallAds(mocked_client, config) - accounts_read_records = iter([{"Id": 180519267, "ParentCustomerId": 100}, {"Id": 180278106, "ParentCustomerId": 200}]) - with patch.object(Accounts, "read_records", return_value=accounts_read_records): - slices = app_install_ads.stream_slices() - assert list(slices) == [{"account_id": 180519267, "customer_id": 100}, {"account_id": 180278106, "customer_id": 200}] - - -@patch.object(source_bing_ads.source, "Client") -def test_bulk_stream_transfrom(mocked_client, config): - record = {"Ad Group": "Ad Group", "App Id": "App Id", "Campaign": "Campaign", "Custom Parameter": "Custom Parameter"} - transformed_record = AppInstallAds(mocked_client, config).transform( - record=record, stream_slice={"account_id": 180519267, "customer_id": 100} - ) - assert transformed_record == { - "Account Id": 180519267, - "Ad Group": "Ad Group", - "App Id": "App Id", - "Campaign": "Campaign", - "Custom Parameter": "Custom Parameter", - } - - -@patch.object(source_bing_ads.source, "Client") -def test_bulk_stream_read_with_chunks(mocked_client, config): - path_to_file = Path(__file__).parent / "app_install_ads.csv" - path_to_file_base = Path(__file__).parent / "app_install_ads_base.csv" - with open(path_to_file_base, "r") as f1, open(path_to_file, "a") as f2: - for line in f1: - f2.write(line) - - app_install_ads = AppInstallAds(mocked_client, config) - result = app_install_ads.read_with_chunks(path=path_to_file) - assert next(result) == { - "Ad Group": "AdGroupNameGoesHere", - "App Id": "AppStoreIdGoesHere", - "App Platform": "Android", - "Campaign": "ParentCampaignNameGoesHere", - "Client Id": "ClientIdGoesHere", - "Custom Parameter": "{_promoCode}=PROMO1; {_season}=summer", - "Destination Url": None, - "Device Preference": "All", - "Display Url": None, - "Final Url": "FinalUrlGoesHere", - "Final Url Suffix": None, - "Id": None, - "Mobile Final Url": None, - "Modified Time": None, - "Name": None, - "Parent Id": "-1111", - "Promotion": None, - "Status": "Active", - "Text": "Find New Customers & Increase Sales!", - "Title": "Contoso Quick Setup", - "Tracking Template": None, - "Type": "App Install Ad", - } - - @patch.object(source_bing_ads.source, "Client") def test_transform(mocked_client, config): record = {"AdFormatPreference": "All", "DevicePreference": 0, "EditorialStatus": "ActiveLimited", "FinalAppUrls": None} @@ -358,147 +204,3 @@ def test_transform(mocked_client, config): "EditorialStatus": "ActiveLimited", "FinalAppUrls": None, } - - -@patch.object(source_bing_ads.source, "Client") -def test_custom_report_clear_namespace(mocked_client, config_with_custom_reports, logger_mock): - custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] - assert custom_report._clear_namespace("tns:ReportAggregation") == "ReportAggregation" - - -@patch.object(source_bing_ads.source, "Client") -def test_custom_report_get_object_columns(mocked_client, config_with_custom_reports, logger_mock): - reporting_service_mock = MagicMock() - reporting_service_mock._get_service_info_dict.return_value = SERVICE_INFO_DICT_V13 - mocked_client.get_service.return_value = reporting_service_mock - mocked_client.environment = "production" - - custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] - - tree = ET.parse(urlparse(SERVICE_INFO_DICT_V13[("reporting", mocked_client.environment)]).path) - request_object = tree.find(f".//{{*}}complexType[@name='{custom_report.report_name}Request']") - - assert custom_report._get_object_columns(request_object, tree) == [ - "TimePeriod", - "AccountId", - "AccountName", - "AccountNumber", - "AccountStatus", - "CampaignId", - "CampaignName", - "CampaignStatus", - "AdGroupId", - "AdGroupName", - "AdGroupStatus", - "AdDistribution", - "Language", - "Network", - "TopVsOther", - "DeviceType", - "DeviceOS", - "BidStrategyType", - "TrackingTemplate", - "CustomParameters", - "DynamicAdTargetId", - "DynamicAdTarget", - "DynamicAdTargetStatus", - "WebsiteCoverage", - "Impressions", - "Clicks", - "Ctr", - "AverageCpc", - "Spend", - "AveragePosition", - "Conversions", - "ConversionRate", - "CostPerConversion", - "Assists", - "Revenue", - "ReturnOnAdSpend", - "CostPerAssist", - "RevenuePerConversion", - "RevenuePerAssist", - "AllConversions", - "AllRevenue", - "AllConversionRate", - "AllCostPerConversion", - "AllReturnOnAdSpend", - "AllRevenuePerConversion", - "ViewThroughConversions", - "Goal", - "GoalType", - "AbsoluteTopImpressionRatePercent", - "TopImpressionRatePercent", - "AverageCpm", - "ConversionsQualified", - "AllConversionsQualified", - "ViewThroughConversionsQualified", - "AdId", - "ViewThroughRevenue", - ] - - -@patch.object(source_bing_ads.source, "Client") -def test_custom_report_send_request(mocked_client, config_with_custom_reports, logger_mock, caplog): - class Fault: - faultstring = "Invalid Client Data" - - custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] - with patch.object(ReportsMixin, "send_request", side_effect=WebFault(fault=Fault(), document=None)): - custom_report.send_request(params={}, customer_id="13131313", account_id="800800808") - assert ( - "Could not sync custom report my test custom report: Please validate your column and aggregation configuration. " - "Error form server: [Invalid Client Data]" - ) in caplog.text - - -@pytest.mark.parametrize( - "aggregation,datastring,expected", - ( - ( - "DayOfWeek", - "1", - 1, - ), - ( - "HourOfDay", - "20", - 20, - ), - ( - "Hourly", - "2022-11-13|10", - 1668333600, - ), - ( - "Hourly", - "2022-11-13|10", - 1668333600, - ), - ( - "Daily", - "2022-11-13", - 1668297600, - ), - ( - "Weekly", - "2022-11-13", - 1668297600, - ), - ( - "Monthly", - "2022-11-13", - 1668297600, - ), - ( - "WeeklyStartingMonday", - "2022-11-13", - 1668297600, - ), - ), -) -@patch.object(source_bing_ads.source, "Client") -def test_custom_report_get_report_record_timestamp(mocked_client, config_with_custom_reports, aggregation, datastring, expected): - custom_report = SourceBingAds().get_custom_reports(config_with_custom_reports, mocked_client)[0] - custom_report.report_aggregation = aggregation - assert custom_report.get_report_record_timestamp(datastring) == expected diff --git a/docs/integrations/sources/bing-ads-migrations.md b/docs/integrations/sources/bing-ads-migrations.md index 3d378d2517adb..c078d1d0cb56a 100644 --- a/docs/integrations/sources/bing-ads-migrations.md +++ b/docs/integrations/sources/bing-ads-migrations.md @@ -1,5 +1,42 @@ # Bing Ads Migration Guide +## Upgrading to 2.0.0 + +This version update affects all hourly reports (end in report_hourly) and the following streams: + +- Accounts +- Campaigns +- Search Query Performance Report +- AppInstallAds +- AppInstallAdLabels +- Labels +- Campaign Labels +- Keyword Labels +- Ad Group Labels +- Keywords +- Budget Summary Report + +All `date` and `date-time` fields will be converted to standard `RFC3339`. Stream state format will be updated as well. + +For the changes to take effect, please refresh the source schema and reset affected streams after you have applied the upgrade. + +| Stream field | Current Airbyte Type | New Airbyte Type | +|-----------------------------|----------------------|-------------------| +| LinkedAgencies | string | object | +| BiddingScheme.MaxCpc.Amount | string | number | +| CostPerConversion | integer | number | +| Modified Time | string | timestamp with tz | +| Date | string | date | +| TimePeriod | string | timestamp with tz | + +Detailed date-time field change examples: + +| Affected streams | Field_name | Old type | New type (`RFC3339`) | +|----------------------------------------------------------------------------------------------------------------------|-----------------|---------------------------|---------------------------------| +| `AppInstallAds`, `AppInstallAdLabels`, `Labels`, `Campaign Labels`, `Keyword Labels`, `Ad Group Labels`, `Keywords` | `Modified Time` | `04/27/2023 18:00:14.970` | `2023-04-27T16:00:14.970+00:00` | +| `Budget Summary Report` | `Date` | `6/10/2021` | `2021-06-10` | +| `* Report Hourly` | `TimePeriod` | `2023-11-04\|11` | `2023-11-04T11:00:00+00:00` | + ## Upgrading to 1.0.0 This version update only affects the geographic performance reports streams. diff --git a/docs/integrations/sources/bing-ads.md b/docs/integrations/sources/bing-ads.md index 1ba6d3570e568..cab52dc73a5bf 100644 --- a/docs/integrations/sources/bing-ads.md +++ b/docs/integrations/sources/bing-ads.md @@ -206,54 +206,55 @@ The Bing Ads API limits the number of requests for all Microsoft Advertising cli ## Changelog -| Version | Date | Pull Request | Subject | -|:--------|:-----------|:---------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------| -| 1.13.0 | 2023-11-13 | [32306](https://github.com/airbytehq/airbyte/pull/32306) | Add Custom reports and decrease backoff max tries number | -| 1.12.1 | 2023-11-10 | [32422](https://github.com/airbytehq/airbyte/pull/32422) | Normalize numeric values in reports | -| 1.12.0 | 2023-11-09 | [32340](https://github.com/airbytehq/airbyte/pull/32340) | Remove default start date in favor of Time Period - Last Year and This Year, if start date is not provided | -| 1.11.0 | 2023-11-06 | [32201](https://github.com/airbytehq/airbyte/pull/32201) | Skip broken CSV report files | -| 1.10.0 | 2023-11-06 | [32148](https://github.com/airbytehq/airbyte/pull/32148) | Add new fields to stream Ads: "BusinessName", "CallToAction", "Headline", "Images", "Videos", "Text" | -| 1.9.0 | 2023-11-03 | [32131](https://github.com/airbytehq/airbyte/pull/32131) | Add "CampaignId", "AccountId", "CustomerId" fields to Ad Groups, Ads and Campaigns streams. | -| 1.8.0 | 2023-11-02 | [32059](https://github.com/airbytehq/airbyte/pull/32059) | Add new streams `CampaignImpressionPerformanceReport` (daily, hourly, weekly, monthly) | -| 1.7.1 | 2023-11-02 | [32088](https://github.com/airbytehq/airbyte/pull/32088) | Raise config error when user does not have accounts | -| 1.7.0 | 2023-11-01 | [32027](https://github.com/airbytehq/airbyte/pull/32027) | Add new streams `AdGroupImpressionPerformanceReport` | -| 1.6.0 | 2023-10-31 | [32008](https://github.com/airbytehq/airbyte/pull/32008) | Add new streams `Keywords` | -| 1.5.0 | 2023-10-30 | [31952](https://github.com/airbytehq/airbyte/pull/31952) | Add new streams `Labels`, `App install ads`, `Keyword Labels`, `Campaign Labels`, `App Install Ad Labels`, `Ad Group Labels` | -| 1.4.0 | 2023-10-27 | [31885](https://github.com/airbytehq/airbyte/pull/31885) | Add new stream: `AccountImpressionPerformanceReport` (daily, hourly, weekly, monthly) | -| 1.3.0 | 2023-10-26 | [31837](https://github.com/airbytehq/airbyte/pull/31837) | Add new stream: `UserLocationPerformanceReport` (daily, hourly, weekly, monthly) | -| 1.2.0 | 2023-10-24 | [31783](https://github.com/airbytehq/airbyte/pull/31783) | Add new stream: `SearchQueryPerformanceReport` (daily, hourly, weekly, monthly) | -| 1.1.0 | 2023-10-24 | [31712](https://github.com/airbytehq/airbyte/pull/31712) | Add new stream: `AgeGenderAudienceReport` (daily, hourly, weekly, monthly) | -| 1.0.2 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | -| 1.0.1 | 2023-10-16 | [31432](https://github.com/airbytehq/airbyte/pull/31432) | Remove primary keys from the geographic performance reports - complete what was missed in version 1.0.0 | -| 1.0.0 | 2023-10-11 | [31277](https://github.com/airbytehq/airbyte/pull/31277) | Remove primary keys from the geographic performance reports. | -| 0.2.3 | 2023-09-28 | [30834](https://github.com/airbytehq/airbyte/pull/30834) | Wrap auth error with the config error. | -| 0.2.2 | 2023-09-27 | [30791](https://github.com/airbytehq/airbyte/pull/30791) | Fix missing fields for geographic performance reports. | -| 0.2.1 | 2023-09-04 | [30128](https://github.com/airbytehq/airbyte/pull/30128) | Add increasing download timeout if ReportingDownloadException occurs | -| 0.2.0 | 2023-08-17 | [27619](https://github.com/airbytehq/airbyte/pull/27619) | Add Geographic Performance Report | -| 0.1.24 | 2023-06-22 | [27619](https://github.com/airbytehq/airbyte/pull/27619) | Retry request after facing temporary name resolution error. | -| 0.1.23 | 2023-05-11 | [25996](https://github.com/airbytehq/airbyte/pull/25996) | Implement a retry logic if SSL certificate validation fails. | -| 0.1.22 | 2023-05-08 | [24223](https://github.com/airbytehq/airbyte/pull/24223) | Add CampaignLabels report column in campaign performance report | -| 0.1.21 | 2023-04-28 | [25668](https://github.com/airbytehq/airbyte/pull/25668) | Add undeclared fields to accounts, campaigns, campaign_performance_report, keyword_performance_report and account_performance_report streams | -| 0.1.20 | 2023-03-09 | [23663](https://github.com/airbytehq/airbyte/pull/23663) | Add lookback window for performance reports in incremental mode | -| 0.1.19 | 2023-03-08 | [23868](https://github.com/airbytehq/airbyte/pull/23868) | Add dimensional-type columns for reports. | -| 0.1.18 | 2023-01-30 | [22073](https://github.com/airbytehq/airbyte/pull/22073) | Fix null values in the `Keyword` column of `keyword_performance_report` streams | -| 0.1.17 | 2022-12-10 | [20005](https://github.com/airbytehq/airbyte/pull/20005) | Add `Keyword` to `keyword_performance_report` stream | -| 0.1.16 | 2022-10-12 | [17873](https://github.com/airbytehq/airbyte/pull/17873) | Fix: added missing campaign types in (Audience, Shopping and DynamicSearchAds) in campaigns stream | -| 0.1.15 | 2022-10-03 | [17505](https://github.com/airbytehq/airbyte/pull/17505) | Fix: limit cache size for ServiceClient instances | -| 0.1.14 | 2022-09-29 | [17403](https://github.com/airbytehq/airbyte/pull/17403) | Fix: limit cache size for ReportingServiceManager instances | -| 0.1.13 | 2022-09-29 | [17386](https://github.com/airbytehq/airbyte/pull/17386) | Migrate to per-stream states. | -| 0.1.12 | 2022-09-05 | [16335](https://github.com/airbytehq/airbyte/pull/16335) | Added backoff for socket.timeout | -| 0.1.11 | 2022-08-25 | [15684](https://github.com/airbytehq/airbyte/pull/15684) (published in [15987](https://github.com/airbytehq/airbyte/pull/15987)) | Fixed log messages being unreadable | -| 0.1.10 | 2022-08-12 | [15602](https://github.com/airbytehq/airbyte/pull/15602) | Fixed bug caused Hourly Reports to crash due to invalid fields set | -| 0.1.9 | 2022-08-02 | [14862](https://github.com/airbytehq/airbyte/pull/14862) | Added missing columns | -| 0.1.8 | 2022-06-15 | [13801](https://github.com/airbytehq/airbyte/pull/13801) | All reports `hourly/daily/weekly/monthly` will be generated by default, these options are removed from input configuration | -| 0.1.7 | 2022-05-17 | [12937](https://github.com/airbytehq/airbyte/pull/12937) | Added OAuth2.0 authentication method, removed `redirect_uri` from input configuration | -| 0.1.6 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy | -| 0.1.5 | 2022-01-01 | [11652](https://github.com/airbytehq/airbyte/pull/11652) | Rebump attempt after DockerHub failure at registring the 0.1.4 | -| 0.1.4 | 2022-03-22 | [11311](https://github.com/airbytehq/airbyte/pull/11311) | Added optional Redirect URI & Tenant ID to spec | -| 0.1.3 | 2022-01-14 | [9510](https://github.com/airbytehq/airbyte/pull/9510) | Fixed broken dependency that blocked connector's operations | -| 0.1.2 | 2021-12-14 | [8429](https://github.com/airbytehq/airbyte/pull/8429) | Update titles and descriptions | -| 0.1.1 | 2021-08-31 | [5750](https://github.com/airbytehq/airbyte/pull/5750) | Added reporting streams\) | -| 0.1.0 | 2021-07-22 | [4911](https://github.com/airbytehq/airbyte/pull/4911) | Initial release supported core streams \(Accounts, Campaigns, Ads, AdGroups\) | +| Version | Date | Pull Request | Subject | +|:--------|:-----------|:---------------------------------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------| +| 2.0.0 | 2023-11-07 | [31995](https://github.com/airbytehq/airbyte/pull/31995) | Schema update for Accounts, Campaigns and Search Query Performance Report streams. Convert `date` and `date-time` fields to standard `RFC3339` | +| 1.13.0 | 2023-11-13 | [32306](https://github.com/airbytehq/airbyte/pull/32306) | Add Custom reports and decrease backoff max tries number | +| 1.12.1 | 2023-11-10 | [32422](https://github.com/airbytehq/airbyte/pull/32422) | Normalize numeric values in reports | +| 1.12.0 | 2023-11-09 | [32340](https://github.com/airbytehq/airbyte/pull/32340) | Remove default start date in favor of Time Period - Last Year and This Year, if start date is not provided | +| 1.11.0 | 2023-11-06 | [32201](https://github.com/airbytehq/airbyte/pull/32201) | Skip broken CSV report files | +| 1.10.0 | 2023-11-06 | [32148](https://github.com/airbytehq/airbyte/pull/32148) | Add new fields to stream Ads: "BusinessName", "CallToAction", "Headline", "Images", "Videos", "Text" | +| 1.9.0 | 2023-11-03 | [32131](https://github.com/airbytehq/airbyte/pull/32131) | Add "CampaignId", "AccountId", "CustomerId" fields to Ad Groups, Ads and Campaigns streams. | +| 1.8.0 | 2023-11-02 | [32059](https://github.com/airbytehq/airbyte/pull/32059) | Add new streams `CampaignImpressionPerformanceReport` (daily, hourly, weekly, monthly) | +| 1.7.1 | 2023-11-02 | [32088](https://github.com/airbytehq/airbyte/pull/32088) | Raise config error when user does not have accounts | +| 1.7.0 | 2023-11-01 | [32027](https://github.com/airbytehq/airbyte/pull/32027) | Add new streams `AdGroupImpressionPerformanceReport` | +| 1.6.0 | 2023-10-31 | [32008](https://github.com/airbytehq/airbyte/pull/32008) | Add new streams `Keywords` | +| 1.5.0 | 2023-10-30 | [31952](https://github.com/airbytehq/airbyte/pull/31952) | Add new streams `Labels`, `App install ads`, `Keyword Labels`, `Campaign Labels`, `App Install Ad Labels`, `Ad Group Labels` | +| 1.4.0 | 2023-10-27 | [31885](https://github.com/airbytehq/airbyte/pull/31885) | Add new stream: `AccountImpressionPerformanceReport` (daily, hourly, weekly, monthly) | +| 1.3.0 | 2023-10-26 | [31837](https://github.com/airbytehq/airbyte/pull/31837) | Add new stream: `UserLocationPerformanceReport` (daily, hourly, weekly, monthly) | +| 1.2.0 | 2023-10-24 | [31783](https://github.com/airbytehq/airbyte/pull/31783) | Add new stream: `SearchQueryPerformanceReport` (daily, hourly, weekly, monthly) | +| 1.1.0 | 2023-10-24 | [31712](https://github.com/airbytehq/airbyte/pull/31712) | Add new stream: `AgeGenderAudienceReport` (daily, hourly, weekly, monthly) | +| 1.0.2 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | +| 1.0.1 | 2023-10-16 | [31432](https://github.com/airbytehq/airbyte/pull/31432) | Remove primary keys from the geographic performance reports - complete what was missed in version 1.0.0 | +| 1.0.0 | 2023-10-11 | [31277](https://github.com/airbytehq/airbyte/pull/31277) | Remove primary keys from the geographic performance reports. | +| 0.2.3 | 2023-09-28 | [30834](https://github.com/airbytehq/airbyte/pull/30834) | Wrap auth error with the config error. | +| 0.2.2 | 2023-09-27 | [30791](https://github.com/airbytehq/airbyte/pull/30791) | Fix missing fields for geographic performance reports. | +| 0.2.1 | 2023-09-04 | [30128](https://github.com/airbytehq/airbyte/pull/30128) | Add increasing download timeout if ReportingDownloadException occurs | +| 0.2.0 | 2023-08-17 | [27619](https://github.com/airbytehq/airbyte/pull/27619) | Add Geographic Performance Report | +| 0.1.24 | 2023-06-22 | [27619](https://github.com/airbytehq/airbyte/pull/27619) | Retry request after facing temporary name resolution error. | +| 0.1.23 | 2023-05-11 | [25996](https://github.com/airbytehq/airbyte/pull/25996) | Implement a retry logic if SSL certificate validation fails. | +| 0.1.22 | 2023-05-08 | [24223](https://github.com/airbytehq/airbyte/pull/24223) | Add CampaignLabels report column in campaign performance report | +| 0.1.21 | 2023-04-28 | [25668](https://github.com/airbytehq/airbyte/pull/25668) | Add undeclared fields to accounts, campaigns, campaign_performance_report, keyword_performance_report and account_performance_report streams | +| 0.1.20 | 2023-03-09 | [23663](https://github.com/airbytehq/airbyte/pull/23663) | Add lookback window for performance reports in incremental mode | +| 0.1.19 | 2023-03-08 | [23868](https://github.com/airbytehq/airbyte/pull/23868) | Add dimensional-type columns for reports. | +| 0.1.18 | 2023-01-30 | [22073](https://github.com/airbytehq/airbyte/pull/22073) | Fix null values in the `Keyword` column of `keyword_performance_report` streams | +| 0.1.17 | 2022-12-10 | [20005](https://github.com/airbytehq/airbyte/pull/20005) | Add `Keyword` to `keyword_performance_report` stream | +| 0.1.16 | 2022-10-12 | [17873](https://github.com/airbytehq/airbyte/pull/17873) | Fix: added missing campaign types in (Audience, Shopping and DynamicSearchAds) in campaigns stream | +| 0.1.15 | 2022-10-03 | [17505](https://github.com/airbytehq/airbyte/pull/17505) | Fix: limit cache size for ServiceClient instances | +| 0.1.14 | 2022-09-29 | [17403](https://github.com/airbytehq/airbyte/pull/17403) | Fix: limit cache size for ReportingServiceManager instances | +| 0.1.13 | 2022-09-29 | [17386](https://github.com/airbytehq/airbyte/pull/17386) | Migrate to per-stream states. | +| 0.1.12 | 2022-09-05 | [16335](https://github.com/airbytehq/airbyte/pull/16335) | Added backoff for socket.timeout | +| 0.1.11 | 2022-08-25 | [15684](https://github.com/airbytehq/airbyte/pull/15684) (published in [15987](https://github.com/airbytehq/airbyte/pull/15987)) | Fixed log messages being unreadable | +| 0.1.10 | 2022-08-12 | [15602](https://github.com/airbytehq/airbyte/pull/15602) | Fixed bug caused Hourly Reports to crash due to invalid fields set | +| 0.1.9 | 2022-08-02 | [14862](https://github.com/airbytehq/airbyte/pull/14862) | Added missing columns | +| 0.1.8 | 2022-06-15 | [13801](https://github.com/airbytehq/airbyte/pull/13801) | All reports `hourly/daily/weekly/monthly` will be generated by default, these options are removed from input configuration | +| 0.1.7 | 2022-05-17 | [12937](https://github.com/airbytehq/airbyte/pull/12937) | Added OAuth2.0 authentication method, removed `redirect_uri` from input configuration | +| 0.1.6 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy | +| 0.1.5 | 2022-01-01 | [11652](https://github.com/airbytehq/airbyte/pull/11652) | Rebump attempt after DockerHub failure at registring the 0.1.4 | +| 0.1.4 | 2022-03-22 | [11311](https://github.com/airbytehq/airbyte/pull/11311) | Added optional Redirect URI & Tenant ID to spec | +| 0.1.3 | 2022-01-14 | [9510](https://github.com/airbytehq/airbyte/pull/9510) | Fixed broken dependency that blocked connector's operations | +| 0.1.2 | 2021-12-14 | [8429](https://github.com/airbytehq/airbyte/pull/8429) | Update titles and descriptions | +| 0.1.1 | 2021-08-31 | [5750](https://github.com/airbytehq/airbyte/pull/5750) | Added reporting streams\) | +| 0.1.0 | 2021-07-22 | [4911](https://github.com/airbytehq/airbyte/pull/4911) | Initial release supported core streams \(Accounts, Campaigns, Ads, AdGroups\) | \ No newline at end of file