diff --git a/airbyte-integrations/connectors/source-amazon-ads/acceptance-test-config.yml b/airbyte-integrations/connectors/source-amazon-ads/acceptance-test-config.yml index eff7c3273bf1..50127adf3e5d 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/acceptance-test-config.yml +++ b/airbyte-integrations/connectors/source-amazon-ads/acceptance-test-config.yml @@ -29,6 +29,8 @@ acceptance_tests: bypass_reason: "can't populate stream because it requires real ad campaign" - name: sponsored_display_creatives bypass_reason: "can't populate stream because it requires real ad campaign" + - name: sponsored_product_ad_group_bid_recommendations + bypass_reason: "data is updated frequently" timeout_seconds: 2400 expect_records: path: integration_tests/expected_records.jsonl diff --git a/airbyte-integrations/connectors/source-amazon-ads/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-amazon-ads/integration_tests/expected_records.jsonl index bf609dee5420..126530502c8a 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-amazon-ads/integration_tests/expected_records.jsonl @@ -48,86 +48,31 @@ {"stream":"sponsored_display_product_ads","data":{"adId":195948665185008,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBOA","state":"enabled"},"emitted_at":1659020219614} {"stream":"sponsored_display_product_ads","data":{"adId":130802512011075,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B091G1HT4P","state":"enabled"},"emitted_at":1659020219614} {"stream":"sponsored_display_targetings","data":{"adGroupId":239470166910761,"bid":0.4,"expression":[{"type":"similarProduct"}],"expressionType":"auto","resolvedExpression":[{"type":"similarProduct"}],"state":"enabled","targetId":124150067548052,"campaignId": 25934734632378},"emitted_at":1659020220625} -{"stream":"sponsored_product_campaigns","data":{"campaignId":39413387973397,"name":"Test campaging for profileId 1861552880916640","campaignType":"sponsoredProducts","targetingType":"manual","premiumBidAdjustment":true,"dailyBudget":10,"ruleBasedBudget":{"isProcessing":false},"startDate":"20220705","endDate":"20220712","state":"paused","bidding":{"strategy":"legacyForSales","adjustments":[{"predicate":"placementTop","percentage":50}]},"tags":{"PONumber":"examplePONumber","accountManager":"exampleAccountManager"}},"emitted_at":1687524797996} -{"stream":"sponsored_product_campaigns","data":{"campaignId":135264288913079,"name":"Campaign - 7/5/2022 18:14:02","campaignType":"sponsoredProducts","targetingType":"auto","premiumBidAdjustment":false,"dailyBudget":10,"startDate":"20220705","state":"enabled","bidding":{"strategy":"legacyForSales","adjustments":[]},"portfolioId":270076898441727},"emitted_at":1687524798170} -{"stream":"sponsored_product_campaigns","data":{"campaignId":191249325250025,"name":"Campaign - 7/8/2022 13:57:48","campaignType":"sponsoredProducts","targetingType":"auto","premiumBidAdjustment":true,"dailyBudget":50,"ruleBasedBudget":{"isProcessing":false},"startDate":"20220708","state":"enabled","bidding":{"strategy":"legacyForSales","adjustments":[{"predicate":"placementProductPage","percentage":100},{"predicate":"placementTop","percentage":100}]},"portfolioId":253945852845204},"emitted_at":1687524798171} -{"stream":"sponsored_product_campaigns","data":{"campaignId":146003174711486,"name":"Test campaging for profileId 3039403378822505","campaignType":"sponsoredProducts","targetingType":"manual","premiumBidAdjustment":true,"dailyBudget":2,"startDate":"20220705","endDate":"20231111","state":"enabled","bidding":{"strategy":"legacyForSales","adjustments":[{"predicate":"placementTop","percentage":50}]},"tags":{"PONumber":"examplePONumber","accountManager":"exampleAccountManager"}},"emitted_at":1687524798327} -{"stream":"sponsored_product_ad_groups","data":{"adGroupId":226404883721634,"name":"My AdGroup for Campaign 39413387973397","campaignId":39413387973397,"defaultBid":10,"state":"enabled"},"emitted_at":1659020222108} -{"stream":"sponsored_product_ad_groups","data":{"adGroupId":183961953969922,"name":"Ad group - 7/5/2022 18:14:02","campaignId":135264288913079,"defaultBid":0.75,"state":"enabled"},"emitted_at":1659020222276} -{"stream":"sponsored_product_ad_groups","data":{"adGroupId":108551155050351,"name":"Ad group - 7/8/2022 13:57:48","campaignId":191249325250025,"defaultBid":1,"state":"enabled"},"emitted_at":1659020222276} -{"stream":"sponsored_product_ad_groups","data":{"adGroupId":103188883625219,"name":"My AdGroup for Campaign 146003174711486","campaignId":146003174711486,"defaultBid":10,"state":"enabled"},"emitted_at":1659020222593} -{"stream":"sponsored_product_keywords","data":{"keywordId":88368653576677,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"keyword1","matchType":"exact","state":"enabled","bid":1.12},"emitted_at":1659020223173} -{"stream":"sponsored_product_keywords","data":{"keywordId":256414981667762,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"test book","matchType":"broad","state":"enabled","bid":1.12},"emitted_at":1659020223174} -{"stream":"sponsored_product_keywords","data":{"keywordId":162522197737998,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"test book","matchType":"phrase","state":"enabled","bid":2.85},"emitted_at":1659020223175} -{"stream":"sponsored_product_keywords","data":{"keywordId":156474025571250,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"test book","matchType":"exact","state":"enabled","bid":1.12},"emitted_at":1659020223175} -{"stream":"sponsored_product_keywords","data":{"keywordId":97960974522677,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"exam book","matchType":"broad","state":"enabled","bid":0.83},"emitted_at":1659020223175} -{"stream":"sponsored_product_keywords","data":{"keywordId":21494218191267,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"exam book","matchType":"phrase","state":"enabled","bid":4.06},"emitted_at":1659020223175} -{"stream":"sponsored_product_keywords","data":{"keywordId":122265145299463,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"exam book","matchType":"exact","state":"enabled","bid":1.12},"emitted_at":1659020223176} -{"stream":"sponsored_product_keywords","data":{"keywordId":105707339702386,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"smartphone","matchType":"broad","state":"enabled","bid":3.52},"emitted_at":1659020223176} -{"stream":"sponsored_product_keywords","data":{"keywordId":185938124401124,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"smartphone","matchType":"phrase","state":"enabled","bid":3.44},"emitted_at":1659020223176} -{"stream":"sponsored_product_keywords","data":{"keywordId":16455263285469,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"smartphone","matchType":"exact","state":"enabled","bid":3.69},"emitted_at":1659020223177} -{"stream":"sponsored_product_negative_keywords","data":{"keywordId":32531566025493,"adGroupId":226404883721634,"campaignId":39413387973397,"keywordText":"negkeyword1","matchType":"negativeExact","state":"enabled"},"emitted_at":1659020224091} -{"stream":"sponsored_product_ads","data":{"adId":134721479349712,"adGroupId":226404883721634,"campaignId":39413387973397,"asin":"B09X3NTQ5S","state":"enabled"},"emitted_at":1659020225056} -{"stream":"sponsored_product_ads","data":{"adId":265970953521535,"adGroupId":226404883721634,"campaignId":39413387973397,"asin":"B09X3QCS24","state":"enabled"},"emitted_at":1659020225057} -{"stream":"sponsored_product_ads","data":{"adId":253366527049144,"adGroupId":226404883721634,"campaignId":39413387973397,"asin":"B09X3P7D6Z","state":"enabled"},"emitted_at":1659020225057} -{"stream":"sponsored_product_ads","data":{"adId":44137758141732,"adGroupId":183961953969922,"campaignId":135264288913079,"asin":"B000VHYM2E","sku":"0R-4KDA-Z2U8","state":"enabled"},"emitted_at":1659020225248} -{"stream":"sponsored_product_ads","data":{"adId":126456292487945,"adGroupId":108551155050351,"campaignId":191249325250025,"asin":"B074K5MDLW","sku":"2J-D6V7-C8XI","state":"enabled"},"emitted_at":1659020225248} -{"stream":"sponsored_product_ads","data":{"adId":125773733335504,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT390","state":"enabled"},"emitted_at":1659020225461} -{"stream":"sponsored_product_ads","data":{"adId":22923447445879,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBJK","state":"enabled"},"emitted_at":1659020225461} -{"stream":"sponsored_product_ads","data":{"adId":174434781640143,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B006K1JR0W","state":"enabled"},"emitted_at":1659020225462} -{"stream":"sponsored_product_ads","data":{"adId":209576432984926,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNV58W","state":"enabled"},"emitted_at":1659020225462} -{"stream":"sponsored_product_ads","data":{"adId":78757678617297,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00U2E0BD0","state":"enabled"},"emitted_at":1659020225462} -{"stream":"sponsored_product_ads","data":{"adId":193756923178712,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBNG","state":"enabled"},"emitted_at":1659020225462} -{"stream":"sponsored_product_ads","data":{"adId":31271769792588,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT38G","state":"enabled"},"emitted_at":1659020225463} -{"stream":"sponsored_product_ads","data":{"adId":150153237605370,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNV596","state":"enabled"},"emitted_at":1659020225463} -{"stream":"sponsored_product_ads","data":{"adId":2074333536480,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00U2E0R66","state":"enabled"},"emitted_at":1659020225463} -{"stream":"sponsored_product_ads","data":{"adId":123533571549424,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNRD2O","state":"enabled"},"emitted_at":1659020225463} -{"stream":"sponsored_product_ads","data":{"adId":217260138761504,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B091FZ92NV","state":"enabled"},"emitted_at":1659020225464} -{"stream":"sponsored_product_ads","data":{"adId":145457886517316,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNRD1U","state":"enabled"},"emitted_at":1659020225464} -{"stream":"sponsored_product_ads","data":{"adId":203822232798249,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00U2E9VEK","state":"enabled"},"emitted_at":1659020225464} -{"stream":"sponsored_product_ads","data":{"adId":117735697461953,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBNQ","state":"enabled"},"emitted_at":1659020225464} -{"stream":"sponsored_product_ads","data":{"adId":142089319699283,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B091G29WN9","state":"enabled"},"emitted_at":1659020225465} -{"stream":"sponsored_product_ads","data":{"adId":95431347262692,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00U2E1JCM","state":"enabled"},"emitted_at":1659020225465} -{"stream":"sponsored_product_ads","data":{"adId":155014902487440,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBJU","state":"enabled"},"emitted_at":1659020225465} -{"stream":"sponsored_product_ads","data":{"adId":11743222321360,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT3AE","state":"enabled"},"emitted_at":1659020225465} -{"stream":"sponsored_product_ads","data":{"adId":103439653344998,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00RW78E52","state":"enabled"},"emitted_at":1659020225466} -{"stream":"sponsored_product_ads","data":{"adId":265969657657801,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT39K","state":"enabled"},"emitted_at":1659020225466} -{"stream":"sponsored_product_ads","data":{"adId":109412610635634,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT39U","state":"enabled"},"emitted_at":1659020225466} -{"stream":"sponsored_product_ads","data":{"adId":136393331771998,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNV59Q","state":"enabled"},"emitted_at":1659020225466} -{"stream":"sponsored_product_ads","data":{"adId":186420999434919,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNRD2E","state":"enabled"},"emitted_at":1659020225467} -{"stream":"sponsored_product_ads","data":{"adId":278853238562368,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B091G35ZDQ","state":"enabled"},"emitted_at":1659020225467} -{"stream":"sponsored_product_ads","data":{"adId":166899201791771,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00U2E1SLE","state":"enabled"},"emitted_at":1659020225467} -{"stream":"sponsored_product_ads","data":{"adId":109280751164007,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B091G3QCHL","state":"enabled"},"emitted_at":1659020225467} -{"stream":"sponsored_product_ads","data":{"adId":151372475824008,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT39A","state":"enabled"},"emitted_at":1659020225467} -{"stream":"sponsored_product_ads","data":{"adId":111491538035732,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00CKZKG20","state":"enabled"},"emitted_at":1659020225468} -{"stream":"sponsored_product_ads","data":{"adId":61045475129398,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B00U2E3HUO","state":"enabled"},"emitted_at":1659020225468} -{"stream":"sponsored_product_ads","data":{"adId":125617015283672,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBO0","state":"enabled"},"emitted_at":1659020225468} -{"stream":"sponsored_product_ads","data":{"adId":183608040922804,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBMW","state":"enabled"},"emitted_at":1659020225468} -{"stream":"sponsored_product_ads","data":{"adId":252975632234287,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNV58M","state":"enabled"},"emitted_at":1659020225469} -{"stream":"sponsored_product_ads","data":{"adId":223374763750850,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNRD2Y","state":"enabled"},"emitted_at":1659020225469} -{"stream":"sponsored_product_ads","data":{"adId":155052344322362,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT37M","state":"enabled"},"emitted_at":1659020225469} -{"stream":"sponsored_product_ads","data":{"adId":210510170479158,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT3AY","state":"enabled"},"emitted_at":1659020225470} -{"stream":"sponsored_product_ads","data":{"adId":179517989169690,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT37W","state":"enabled"},"emitted_at":1659020225470} -{"stream":"sponsored_product_ads","data":{"adId":163992879107492,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNV5AA","state":"enabled"},"emitted_at":1659020225470} -{"stream":"sponsored_product_ads","data":{"adId":103527738992867,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNT386","state":"enabled"},"emitted_at":1659020225470} -{"stream":"sponsored_product_ads","data":{"adId":195948665185008,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B000BNQBOA","state":"enabled"},"emitted_at":1659020225470} -{"stream":"sponsored_product_ads","data":{"adId":130802512011075,"adGroupId":239470166910761,"campaignId":25934734632378,"asin":"B091G1HT4P","state":"enabled"},"emitted_at":1659020225471} -{"stream":"sponsored_product_targetings","data":{"targetId":50319181484813,"adGroupId":183961953969922,"campaignId":135264288913079,"expressionType":"auto","state":"enabled","expression":[{"type":"queryHighRelMatches"}],"resolvedExpression":[{"type":"queryHighRelMatches"}]},"emitted_at":1659020226434} -{"stream":"sponsored_product_targetings","data":{"targetId":27674318672023,"adGroupId":183961953969922,"campaignId":135264288913079,"expressionType":"auto","state":"enabled","expression":[{"type":"queryBroadRelMatches"}],"resolvedExpression":[{"type":"queryBroadRelMatches"}]},"emitted_at":1659020226435} -{"stream":"sponsored_product_targetings","data":{"targetId":231060819625654,"adGroupId":183961953969922,"campaignId":135264288913079,"expressionType":"auto","state":"enabled","expression":[{"type":"asinAccessoryRelated"}],"resolvedExpression":[{"type":"asinAccessoryRelated"}]},"emitted_at":1659020226435} -{"stream":"sponsored_product_targetings","data":{"targetId":223980840024498,"adGroupId":183961953969922,"campaignId":135264288913079,"expressionType":"auto","state":"enabled","expression":[{"type":"asinSubstituteRelated"}],"resolvedExpression":[{"type":"asinSubstituteRelated"}]},"emitted_at":1659020226436} -{"stream":"sponsored_product_targetings","data":{"targetId":62579800516352,"adGroupId":108551155050351,"campaignId":191249325250025,"expressionType":"auto","state":"enabled","expression":[{"type":"queryHighRelMatches"}],"resolvedExpression":[{"type":"queryHighRelMatches"}]},"emitted_at":1659020226436} -{"stream":"sponsored_product_targetings","data":{"targetId":232221427954900,"adGroupId":108551155050351,"campaignId":191249325250025,"expressionType":"auto","state":"enabled","expression":[{"type":"queryBroadRelMatches"}],"resolvedExpression":[{"type":"queryBroadRelMatches"}]},"emitted_at":1659020226436} -{"stream":"sponsored_product_targetings","data":{"targetId":12739477778779,"adGroupId":108551155050351,"campaignId":191249325250025,"expressionType":"auto","state":"enabled","expression":[{"type":"asinAccessoryRelated"}],"resolvedExpression":[{"type":"asinAccessoryRelated"}]},"emitted_at":1659020226436} -{"stream":"sponsored_product_targetings","data":{"targetId":1189452552122,"adGroupId":108551155050351,"campaignId":191249325250025,"expressionType":"auto","state":"enabled","expression":[{"type":"asinSubstituteRelated"}],"resolvedExpression":[{"type":"asinSubstituteRelated"}]},"emitted_at":1659020226437} -{"stream":"sponsored_product_campaign_negative_keywords","data":{"keywordId":367623300145491,"campaignId":39413387973397,"keywordText":"campaign negative keyword","matchType":"negativeExact","state":"enabled"},"emitted_at":1687197089168} -{"stream":"sponsored_product_campaign_negative_keywords","data":{"keywordId":313227767817048,"campaignId":39413387973397,"keywordText":"campaign negative keyword2","matchType":"negativeExact","state":"enabled"},"emitted_at":1687197089170} -{"stream":"sponsored_product_campaign_negative_keywords","data":{"keywordId":362264772936192,"campaignId":191249325250025,"keywordText":"negative","matchType":"negativeExact","state":"enabled"},"emitted_at":1687197089322} -{"stream":"sponsored_product_campaign_negative_keywords","data":{"keywordId":318852596875190,"campaignId":191249325250025,"keywordText":"negative phrase","matchType":"negativeExact","state":"enabled"},"emitted_at":1687197089323} -{"stream":"sponsored_product_campaign_negative_keywords","data":{"keywordId":435410875929367,"campaignId":191249325250025,"keywordText":"another negative phrase","matchType":"negativeExact","state":"enabled"},"emitted_at":1687197089324} {"stream":"sponsored_display_budget_rules","data":{"createdDate":1657024512836,"lastUpdatedDate":1657024512836,"ruleDetails":{"budgetIncreaseBy":{"type":"PERCENT","value":32},"duration":{"dateRangeTypeRuleDuration":null,"eventTypeRuleDuration":{"endDate":"20220713","eventId":"ae0226d3-9f97-5122-a749-2e9ba741a2dc","eventName":"Prime Day","startDate":"20220712"}},"name":"ex","performanceMeasureCondition":null,"recurrence":{"daysOfWeek":null,"intraDaySchedule":null,"type":"DAILY"},"ruleType":"SCHEDULE"},"ruleId":"b5abeec6-7624-49e7-8571-97b8ba61551e","ruleState":"ACTIVE","ruleStatus":"EXPIRED","ruleStatusDetails":null},"emitted_at":1687254964816} {"stream":"sponsored_display_budget_rules","data":{"createdDate":1686765545918,"lastUpdatedDate":1686765545918,"ruleDetails":{"budgetIncreaseBy":{"type":"PERCENT","value":1},"duration":{"dateRangeTypeRuleDuration":null,"eventTypeRuleDuration":{"endDate":"20230619","eventId":"553ddee0-8178-544b-a54a-f8918d21ad5f","eventName":"Father's Day","startDate":"20230611"}},"name":"Rule for vadim","performanceMeasureCondition":null,"recurrence":{"daysOfWeek":null,"intraDaySchedule":null,"type":"DAILY"},"ruleType":"SCHEDULE"},"ruleId":"039ff522-f785-4409-8f3a-f6f884ec1750","ruleState":"ACTIVE","ruleStatus":"EXPIRED","ruleStatusDetails":null},"emitted_at":1687254965077} {"stream": "portfolios", "data": {"portfolioId": 253945852845204, "name": "Test Portfolio 2", "inBudget": true, "state": "enabled", "creationDate": 1687510907465, "lastUpdatedDate": 1687510907465, "servingStatus": "PORTFOLIO_STATUS_ENABLED"}, "emitted_at": 1688475309870} {"stream": "portfolios", "data": {"portfolioId": 270076898441727, "name": "Test Portfolio", "budget": {"amount": 1.0, "currencyCode": "USD", "policy": "dateRange", "startDate": "20230623", "endDate": "20230624"}, "inBudget": true, "state": "enabled", "creationDate": 1687510616329, "lastUpdatedDate": 1687514774484, "servingStatus": "PORTFOLIO_ENDED"}, "emitted_at": 1688475309871} -{"stream":"sponsored_product_ad_group_suggested_keywords","data":{"adGroupId":103188883625219,"suggestedKeywords":[{"keywordText":"disposable hotel slippers","matchType":"broad"},{"keywordText":"hotel slippers women","matchType":"broad"},{"keywordText":"slippers bulk","matchType":"broad"},{"keywordText":"spa slipper","matchType":"broad"},{"keywordText":"disposable guest slippers","matchType":"broad"},{"keywordText":"hotel slipper","matchType":"broad"},{"keywordText":"black bulk slippers","matchType":"broad"},{"keywordText":"disposable black slippers","matchType":"broad"},{"keywordText":"toothbrush oral b medium","matchType":"broad"},{"keywordText":"toothbrush soft oral b","matchType":"broad"},{"keywordText":"diamond cat food wet","matchType":"broad"},{"keywordText":"toothbrush 1 count","matchType":"broad"},{"keywordText":"black slipper pack","matchType":"broad"},{"keywordText":"toothbrush medium","matchType":"broad"},{"keywordText":"peach mango propel water","matchType":"broad"},{"keywordText":"black guest slippers","matchType":"broad"},{"keywordText":"black hotel slippers","matchType":"broad"},{"keywordText":"black house slippers bulk","matchType":"broad"},{"keywordText":"black spa slippers","matchType":"broad"},{"keywordText":"diamond natural wet cat food","matchType":"broad"},{"keywordText":"house slippers 6 pack","matchType":"broad"},{"keywordText":"house slippers guests bulk","matchType":"broad"},{"keywordText":"single toothbrush","matchType":"broad"},{"keywordText":"spa slippers women black","matchType":"broad"},{"keywordText":"toothbrush oral b manual","matchType":"broad"},{"keywordText":"medium toothbrush single","matchType":"broad"},{"keywordText":"toothbrush charcoal","matchType":"broad"},{"keywordText":"toothbrush hard","matchType":"broad"},{"keywordText":"tooth brush medium","matchType":"broad"},{"keywordText":"tooth brush soft","matchType":"broad"},{"keywordText":"house slippers disposable","matchType":"broad"},{"keywordText":"toothbrush oral b","matchType":"broad"},{"keywordText":"toothbrush soft extra","matchType":"broad"},{"keywordText":"black disposable slippers guests","matchType":"broad"},{"keywordText":"bulk slippers guests washable","matchType":"broad"},{"keywordText":"crest toothbrush","matchType":"broad"},{"keywordText":"toothbrush travel","matchType":"broad"},{"keywordText":"black spa slippers bulk","matchType":"broad"},{"keywordText":"house slippers bulk","matchType":"broad"},{"keywordText":"house slippers visitor","matchType":"broad"},{"keywordText":"disposable slipper","matchType":"broad"},{"keywordText":"spa slippers disposable black","matchType":"broad"},{"keywordText":"toothbrush small","matchType":"broad"},{"keywordText":"cepillo de dientes","matchType":"broad"},{"keywordText":"guest slippers bulk","matchType":"broad"},{"keywordText":"soft toothbrush","matchType":"broad"},{"keywordText":"spa house slippers","matchType":"broad"},{"keywordText":"toothbrush firm","matchType":"broad"},{"keywordText":"toothbrush sensitive","matchType":"broad"},{"keywordText":"bulk pack slippers","matchType":"broad"},{"keywordText":"house guest slippers","matchType":"broad"},{"keywordText":"propel peach water","matchType":"broad"},{"keywordText":"teeth brush","matchType":"broad"},{"keywordText":"tooth brush oral b","matchType":"broad"},{"keywordText":"toothbrush amazon fresh","matchType":"broad"},{"keywordText":"toothbrush oralb","matchType":"broad"},{"keywordText":"toothbrush whitening","matchType":"broad"},{"keywordText":"toothbrusj","matchType":"broad"},{"keywordText":"water propel","matchType":"broad"},{"keywordText":"extra soft tooth brush","matchType":"broad"},{"keywordText":"house slippers guests washable","matchType":"broad"},{"keywordText":"propel peach mango","matchType":"broad"},{"keywordText":"tootbrush","matchType":"broad"},{"keywordText":"toothbrush","matchType":"broad"},{"keywordText":"toothbrush bamboo","matchType":"broad"},{"keywordText":"diamond naturals canned cat food","matchType":"broad"},{"keywordText":"organic potato","matchType":"broad"},{"keywordText":"spa slippers bulk","matchType":"broad"},{"keywordText":"toothbrush oral b white","matchType":"broad"},{"keywordText":"toothbrush soft bristle","matchType":"broad"},{"keywordText":"slippers 12 pair","matchType":"broad"},{"keywordText":"black house guest slippers","matchType":"broad"},{"keywordText":"black house slippers pack","matchType":"broad"},{"keywordText":"black slippers set","matchType":"broad"},{"keywordText":"black washable slippers","matchType":"broad"},{"keywordText":"black washable spa slippers","matchType":"broad"},{"keywordText":"bulk house shoes guests","matchType":"broad"},{"keywordText":"diamond naturals cat food can","matchType":"broad"},{"keywordText":"diamond naturals kitten food wet","matchType":"broad"},{"keywordText":"dispisable slippers","matchType":"broad"},{"keywordText":"disposable house slippers black","matchType":"broad"},{"keywordText":"disposable spa slippers bulk","matchType":"broad"},{"keywordText":"disposable washable slippers","matchType":"broad"},{"keywordText":"disposal house slippers","matchType":"broad"},{"keywordText":"fisposable slippers","matchType":"broad"},{"keywordText":"guest slippers washable set","matchType":"broad"},{"keywordText":"hoise slippers","matchType":"broad"},{"keywordText":"home slipper set","matchType":"broad"},{"keywordText":"house alippers guests","matchType":"broad"},{"keywordText":"house shoes guests washable","matchType":"broad"},{"keywordText":"house slipeprs","matchType":"broad"},{"keywordText":"disposable house slippers guest","matchType":"broad"},{"keywordText":"disposable slippers women","matchType":"broad"},{"keywordText":"disposable spa slippers","matchType":"broad"},{"keywordText":"hotel slippers bulk","matchType":"broad"},{"keywordText":"disposable slippers travel","matchType":"broad"},{"keywordText":"one time use slippers","matchType":"broad"},{"keywordText":"pack slippers guest","matchType":"broad"},{"keywordText":"guest slipper","matchType":"broad"},{"keywordText":"guest slippers washable","matchType":"broad"}]},"emitted_at":1688632533382} -{"stream":"sponsored_product_ad_group_bid_recommendations","data":{"adGroupId":183961953969922,"suggestedBid":{"rangeEnd":1.71,"rangeStart":0.14,"suggested":0.62}},"emitted_at":1688632722904} +{"stream": "sponsored_product_campaigns", "data": {"budget": {"budget": 10.0, "budgetType": "DAILY"}, "campaignId": "135264288913079", "dynamicBidding": {"placementBidding": [], "strategy": "LEGACY_FOR_SALES"}, "name": "Campaign - 7/5/2022 18:14:02", "portfolioId": "270076898441727", "startDate": "2022-07-05", "state": "ENABLED", "tags": {}, "targetingType": "AUTO"}, "emitted_at": 1710888592084} +{"stream": "sponsored_product_campaigns", "data": {"budget": {"budget": 50.0, "budgetType": "DAILY"}, "campaignId": "191249325250025", "dynamicBidding": {"placementBidding": [{"percentage": 100, "placement": "PLACEMENT_PRODUCT_PAGE"}, {"percentage": 100, "placement": "PLACEMENT_TOP"}], "strategy": "LEGACY_FOR_SALES"}, "name": "Campaign - 7/8/2022 13:57:48", "portfolioId": "253945852845204", "startDate": "2022-07-08", "state": "ENABLED", "tags": {}, "targetingType": "AUTO"}, "emitted_at": 1710888592085} +{"stream": "sponsored_product_campaigns", "data": {"budget": {"budget": 10.0, "budgetType": "DAILY"}, "campaignId": "39413387973397", "dynamicBidding": {"placementBidding": [{"percentage": 50, "placement": "PLACEMENT_TOP"}], "strategy": "LEGACY_FOR_SALES"}, "endDate": "2022-07-12", "name": "Test campaging for profileId 1861552880916640#l1iwuw7s954", "startDate": "2022-07-05", "state": "PAUSED", "tags": {"PONumber": "examplePONumber", "accountManager": "exampleAccountManager"}, "targetingType": "MANUAL"}, "emitted_at": 1710888591878} +{"stream": "sponsored_product_ad_groups", "data": {"adGroupId": "183961953969922", "campaignId": "135264288913079", "defaultBid": 0.75, "name": "Ad group - 7/5/2022 18:14:02", "state": "ENABLED"}, "emitted_at": 1710888605592} +{"stream": "sponsored_product_ad_groups", "data": {"adGroupId": "226404883721634", "campaignId": "39413387973397", "defaultBid": 10.0, "name": "My AdGroup for Campaign 39413387973397", "state": "ENABLED"}, "emitted_at": 1710888605408} +{"stream": "sponsored_product_ad_groups", "data": {"adGroupId": "108551155050351", "campaignId": "191249325250025", "defaultBid": 1.0, "name": "Ad group - 7/8/2022 13:57:48", "state": "ENABLED"}, "emitted_at": 1710888605592} +{"stream": "sponsored_product_ad_groups", "data": {"adGroupId": "475489269904624", "campaignId": "556045554720184", "defaultBid": 0.75, "name": "Ad group - 7/5/2023 20:50:20.159", "state": "ENABLED"}, "emitted_at": 1710891690912} +{"stream": "sponsored_product_ad_groups", "data": {"adGroupId": "103188883625219", "campaignId": "146003174711486", "defaultBid": 10.0, "name": "My AdGroup for Campaign 146003174711486", "state": "ENABLED"}, "emitted_at": 1710891691112} +{"stream": "sponsored_product_keywords", "data": {"adGroupId": "226404883721634", "bid": 1.12, "campaignId": "39413387973397", "keywordId": "88368653576677", "keywordText": "keyword1", "matchType": "EXACT", "state": "ENABLED"}, "emitted_at": 1710888676159} +{"stream": "sponsored_product_keywords", "data": {"adGroupId": "226404883721634", "bid": 2.85, "campaignId": "39413387973397", "keywordId": "162522197737998", "keywordText": "test book", "matchType": "PHRASE", "state": "ENABLED"}, "emitted_at": 1710888676160} +{"stream": "sponsored_product_keywords", "data": {"adGroupId": "226404883721634", "bid": 1.12, "campaignId": "39413387973397", "keywordId": "256414981667762", "keywordText": "test book", "matchType": "BROAD", "state": "ENABLED"}, "emitted_at": 1710888676160} +{"stream": "sponsored_product_negative_keywords", "data": {"adGroupId": "226404883721634", "campaignId": "39413387973397", "keywordId": "32531566025493", "keywordText": "negkeyword1", "matchType": "NEGATIVE_EXACT", "state": "ENABLED"}, "emitted_at": 1710888687416} +{"stream": "sponsored_product_ads", "data": {"adGroupId": "226404883721634", "adId": "134721479349712", "asin": "B09X3NTQ5S", "campaignId": "39413387973397", "state": "ENABLED"}, "emitted_at": 1710888734046} +{"stream": "sponsored_product_ads", "data": {"adGroupId": "226404883721634", "adId": "253366527049144", "asin": "B09X3P7D6Z", "campaignId": "39413387973397", "state": "ENABLED"}, "emitted_at": 1710888734047} +{"stream": "sponsored_product_ads", "data": {"adGroupId": "226404883721634", "adId": "265970953521535", "asin": "B09X3QCS24", "campaignId": "39413387973397", "state": "ENABLED"}, "emitted_at": 1710888734047} +{"stream": "sponsored_product_targetings", "data": {"adGroupId": "183961953969922", "campaignId": "135264288913079", "expression": [{"type": "ASIN_ACCESSORY_RELATED"}], "expressionType": "AUTO", "resolvedExpression": [{"type": "ASIN_ACCESSORY_RELATED"}], "state": "ENABLED", "targetId": "231060819625654"}, "emitted_at": 1710888741880} +{"stream": "sponsored_product_targetings", "data": {"adGroupId": "183961953969922", "campaignId": "135264288913079", "expression": [{"type": "QUERY_BROAD_REL_MATCHES"}], "expressionType": "AUTO", "resolvedExpression": [{"type": "QUERY_BROAD_REL_MATCHES"}], "state": "ENABLED", "targetId": "27674318672023"}, "emitted_at": 1710888741880} +{"stream": "sponsored_product_targetings", "data": {"adGroupId": "183961953969922", "campaignId": "135264288913079", "expression": [{"type": "QUERY_HIGH_REL_MATCHES"}], "expressionType": "AUTO", "resolvedExpression": [{"type": "QUERY_HIGH_REL_MATCHES"}], "state": "ENABLED", "targetId": "50319181484813"}, "emitted_at": 1710888741879} +{"stream": "sponsored_product_campaign_negative_keywords", "data": {"campaignId": "191249325250025", "keywordId": "362264772936192", "keywordText": "negative", "matchType": "NEGATIVE_EXACT", "state": "ENABLED"}, "emitted_at": 1710888790876} +{"stream": "sponsored_product_campaign_negative_keywords", "data": {"campaignId": "39413387973397", "keywordId": "313227767817048", "keywordText": "campaign negative keyword2", "matchType": "NEGATIVE_EXACT", "state": "ENABLED"}, "emitted_at": 1710888790672} +{"stream": "sponsored_product_campaign_negative_keywords", "data": {"campaignId": "39413387973397", "keywordId": "367623300145491", "keywordText": "campaign negative keyword", "matchType": "NEGATIVE_EXACT", "state": "ENABLED"}, "emitted_at": 1710888790671} +{"stream": "sponsored_product_ad_group_suggested_keywords", "data": {"adGroupId": 475489269904624, "suggestedKeywords": []}, "emitted_at": 1710889475987} +{"stream": "sponsored_product_ad_group_suggested_keywords", "data": {"adGroupId": 103188883625219, "suggestedKeywords": [{"keywordText": "guest slipper", "matchType": "broad"}, {"keywordText": "bulk hotel slippers", "matchType": "broad"}, {"keywordText": "hotel slippers women", "matchType": "broad"}, {"keywordText": "slippers bulk", "matchType": "broad"}, {"keywordText": "spa slipper", "matchType": "broad"}, {"keywordText": "disposable slipper", "matchType": "broad"}, {"keywordText": "hotel slippers", "matchType": "broad"}, {"keywordText": "disposable guest slippers", "matchType": "broad"}, {"keywordText": "house guest slippers", "matchType": "broad"}, {"keywordText": "oral b stain eraser toothbrush", "matchType": "broad"}, {"keywordText": "black bulk slippers", "matchType": "broad"}, {"keywordText": "disposable black slippers", "matchType": "broad"}, {"keywordText": "toothbrush oral b medium", "matchType": "broad"}, {"keywordText": "toothbrush soft oral b", "matchType": "broad"}, {"keywordText": "diamond cat food wet", "matchType": "broad"}, {"keywordText": "toothbrush 1 count", "matchType": "broad"}, {"keywordText": "black slipper pack", "matchType": "broad"}, {"keywordText": "toothbrush medium", "matchType": "broad"}, {"keywordText": "peach mango propel water", "matchType": "broad"}, {"keywordText": "black guest slippers", "matchType": "broad"}, {"keywordText": "black hotel slippers", "matchType": "broad"}, {"keywordText": "black house slippers bulk", "matchType": "broad"}, {"keywordText": "black spa slippers", "matchType": "broad"}, {"keywordText": "diamond natural wet cat food", "matchType": "broad"}, {"keywordText": "house slippers 6 pack", "matchType": "broad"}, {"keywordText": "house slippers guests bulk", "matchType": "broad"}, {"keywordText": "single toothbrush", "matchType": "broad"}, {"keywordText": "spa slippers women black", "matchType": "broad"}, {"keywordText": "toothbrush oral b manual", "matchType": "broad"}, {"keywordText": "medium toothbrush single", "matchType": "broad"}, {"keywordText": "toothbrush charcoal", "matchType": "broad"}, {"keywordText": "toothbrush hard", "matchType": "broad"}, {"keywordText": "tooth brush medium", "matchType": "broad"}, {"keywordText": "tooth brush soft", "matchType": "broad"}, {"keywordText": "house slippers disposable", "matchType": "broad"}, {"keywordText": "toothbrush oral b", "matchType": "broad"}, {"keywordText": "toothbrush soft extra", "matchType": "broad"}, {"keywordText": "black disposable slippers guests", "matchType": "broad"}, {"keywordText": "bulk slippers guests washable", "matchType": "broad"}, {"keywordText": "crest toothbrush", "matchType": "broad"}, {"keywordText": "toothbrush travel", "matchType": "broad"}, {"keywordText": "black spa slippers bulk", "matchType": "broad"}, {"keywordText": "house slippers bulk", "matchType": "broad"}, {"keywordText": "house slippers visitor", "matchType": "broad"}, {"keywordText": "spa slippers disposable black", "matchType": "broad"}, {"keywordText": "toothbrush small", "matchType": "broad"}, {"keywordText": "cepillo de dientes", "matchType": "broad"}, {"keywordText": "guest slippers bulk", "matchType": "broad"}, {"keywordText": "soft toothbrush", "matchType": "broad"}, {"keywordText": "spa house slippers", "matchType": "broad"}, {"keywordText": "toothbrush firm", "matchType": "broad"}, {"keywordText": "toothbrush sensitive", "matchType": "broad"}, {"keywordText": "bulk pack slippers", "matchType": "broad"}, {"keywordText": "propel peach water", "matchType": "broad"}, {"keywordText": "teeth brush", "matchType": "broad"}, {"keywordText": "tooth brush oral b", "matchType": "broad"}, {"keywordText": "toothbrush amazon fresh", "matchType": "broad"}, {"keywordText": "toothbrush oralb", "matchType": "broad"}, {"keywordText": "toothbrush whitening", "matchType": "broad"}, {"keywordText": "toothbrusj", "matchType": "broad"}, {"keywordText": "water propel", "matchType": "broad"}, {"keywordText": "extra soft tooth brush", "matchType": "broad"}, {"keywordText": "house slippers guests washable", "matchType": "broad"}, {"keywordText": "propel peach mango", "matchType": "broad"}, {"keywordText": "tootbrush", "matchType": "broad"}, {"keywordText": "toothbrush", "matchType": "broad"}, {"keywordText": "toothbrush bamboo", "matchType": "broad"}, {"keywordText": "diamond naturals canned cat food", "matchType": "broad"}, {"keywordText": "organic potato", "matchType": "broad"}, {"keywordText": "spa slippers bulk", "matchType": "broad"}, {"keywordText": "toothbrush oral b white", "matchType": "broad"}, {"keywordText": "toothbrush soft bristle", "matchType": "broad"}, {"keywordText": "12 pairs slippers", "matchType": "broad"}, {"keywordText": "black house guest slippers", "matchType": "broad"}, {"keywordText": "black house slippers pack", "matchType": "broad"}, {"keywordText": "black slippers set", "matchType": "broad"}, {"keywordText": "black washable slippers", "matchType": "broad"}, {"keywordText": "black washable spa slippers", "matchType": "broad"}, {"keywordText": "bulk house shoes guests", "matchType": "broad"}, {"keywordText": "diamond naturals cat food can", "matchType": "broad"}, {"keywordText": "diamond naturals kitten food wet", "matchType": "broad"}, {"keywordText": "dispisable slippers", "matchType": "broad"}, {"keywordText": "disposable house slippers black", "matchType": "broad"}, {"keywordText": "disposable spa slippers bulk", "matchType": "broad"}, {"keywordText": "disposable washable slippers", "matchType": "broad"}, {"keywordText": "disposal house slippers", "matchType": "broad"}, {"keywordText": "fisposable slippers", "matchType": "broad"}, {"keywordText": "guest slippers washable set", "matchType": "broad"}, {"keywordText": "hoise slippers", "matchType": "broad"}, {"keywordText": "home slipper set", "matchType": "broad"}, {"keywordText": "house alippers guests", "matchType": "broad"}, {"keywordText": "guest house shoes washable", "matchType": "broad"}, {"keywordText": "house slipeprs", "matchType": "broad"}, {"keywordText": "disposable hotel slippers", "matchType": "broad"}, {"keywordText": "disposable house slippers guest", "matchType": "broad"}, {"keywordText": "disposable slippers women", "matchType": "broad"}, {"keywordText": "disposable spa slippers", "matchType": "broad"}, {"keywordText": "disposable slippers travel", "matchType": "broad"}, {"keywordText": "one time use slippers", "matchType": "broad"}, {"keywordText": "pack slippers guest", "matchType": "broad"}]}, "emitted_at": 1710889477445} +{"stream": "sponsored_product_ad_group_suggested_keywords", "data": {"adGroupId": 226404883721634, "suggestedKeywords": []}, "emitted_at": 1710889474524} diff --git a/airbyte-integrations/connectors/source-amazon-ads/metadata.yaml b/airbyte-integrations/connectors/source-amazon-ads/metadata.yaml index d96b7004533b..fdf71b9777f4 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/metadata.yaml +++ b/airbyte-integrations/connectors/source-amazon-ads/metadata.yaml @@ -13,7 +13,7 @@ data: connectorSubtype: api connectorType: source definitionId: c6b0a29e-1da9-4512-9002-7bfd0cba2246 - dockerImageTag: 4.1.0 + dockerImageTag: 5.0.0 dockerRepository: airbyte/source-amazon-ads documentationUrl: https://docs.airbyte.com/integrations/sources/amazon-ads githubIssueLabel: source-amazon-ads @@ -32,6 +32,18 @@ data: releaseStage: generally_available releases: breakingChanges: + 5.0.0: + message: "`SponsoredBrandCampaigns`, `SponsoredBrandsAdGroups`, `SponsoredProductCampaigns`, and `SponsoredProductAdGroupBidRecommendations` streams have updated schemas and must be reset." + upgradeDeadline: "2024-03-27" + scopedImpact: + - scopeType: stream + impactedScopes: + [ + "sponsored_brands_campaigns", + "sponsored_brands_ad_groups", + "sponsored_product_campaigns", + "sponsored_product_ad_group_bid_recommendations", + ] 4.0.0: message: "Streams `SponsoredBrandsAdGroups` and `SponsoredBrandsKeywords` now have updated schemas." upgradeDeadline: "2024-01-17" diff --git a/airbyte-integrations/connectors/source-amazon-ads/pyproject.toml b/airbyte-integrations/connectors/source-amazon-ads/pyproject.toml index 68d7e35fe3e0..38d0e64b3876 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/pyproject.toml +++ b/airbyte-integrations/connectors/source-amazon-ads/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",] build-backend = "poetry.core.masonry.api" [tool.poetry] -version = "4.1.0" +version = "5.0.0" name = "source-amazon-ads" description = "Source implementation for Amazon Ads." authors = [ "Airbyte ",] diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/__init__.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/__init__.py index e9c8aae725fc..053c733ac2ff 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/__init__.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/__init__.py @@ -2,9 +2,18 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # from .attribution_report import AttributionReportModel -from .common import CatalogModel, Keywords, MetricsReport, NegativeKeywords, Portfolio +from .common import ( + CatalogModel, + Keywords, + MetricsReport, + NegativeKeywords, + Portfolio +) from .profile import Profile -from .sponsored_brands import BrandsAdGroup, BrandsCampaign +from .sponsored_brands import ( + BrandsAdGroup, + BrandsCampaign, +) from .sponsored_display import DisplayAdGroup, DisplayBudgetRules, DisplayCampaign, DisplayCreatives, DisplayProductAds, DisplayTargeting from .sponsored_products import ( ProductAd, @@ -13,6 +22,9 @@ ProductAdGroupSuggestedKeywords, ProductCampaign, ProductTargeting, + SponsoredProductCampaignNegativeKeywordsModel, + SponsoredProductKeywordsModel, + SponsoredProductNegativeKeywordsModel ) __all__ = [ @@ -28,6 +40,7 @@ "DisplayCreatives", "MetricsReport", "NegativeKeywords", + "CampaignNegativeKeywords", "Portfolio", "ProductAd", "ProductAdGroups", @@ -37,4 +50,7 @@ "ProductTargeting", "Profile", "AttributionReportModel", + "SponsoredProductCampaignNegativeKeywordsModel", + "SponsoredProductKeywordsModel", + "SponsoredProductNegativeKeywordsModel" ] diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_brands.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_brands.py index e7e2fa7cd07c..51d8f091e81c 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_brands.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_brands.py @@ -3,7 +3,7 @@ # from decimal import Decimal -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional from .common import CatalogModel @@ -27,7 +27,7 @@ class Creative(CatalogModel): class BrandsCampaign(CatalogModel): - campaignId: Decimal + campaignId: str name: str tags: Dict[str, str] budget: Decimal @@ -35,25 +35,19 @@ class BrandsCampaign(CatalogModel): startDate: str endDate: str state: str - servingStatus: str brandEntityId: str - portfolioId: int - bidOptimization: bool = None - bidMultiplier: Decimal = None - adFormat: str - bidAdjustments: Optional[List[BidAdjustment]] - creative: Optional[Creative] - landingPage: Optional[LandingPage] - supplySource: Optional[str] + portfolioId: str + ruleBasedBudget: Optional[Dict[str, Any]] + bidding: Optional[Dict[str, Any]] + productLocation: Optional[str] + costType: Optional[str] + smartDefault: Optional[List[str]] + extendedData: Optional[Dict[str, Any]] class BrandsAdGroup(CatalogModel): - campaignId: Decimal - adGroupId: Decimal + campaignId: str + adGroupId: str name: str - bid: Decimal - keywordId: Decimal - keywordText: str - nativeLanguageKeyword: str - matchType: str state: str + extendedData: Dict[str, Any] diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_products.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_products.py index b5ca604e06b6..6ef9a7b5ff1d 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_products.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/schemas/sponsored_products.py @@ -3,9 +3,9 @@ # from decimal import Decimal -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional -from .common import CatalogModel, Targeting +from .common import CatalogModel, KeywordsBase class Adjustments(CatalogModel): @@ -19,39 +19,38 @@ class Bidding(CatalogModel): class ProductCampaign(CatalogModel): - portfolioId: int - campaignId: Decimal + portfolioId: str + campaignId: str name: str tags: Dict[str, str] - campaignType: str targetingType: str state: str - dailyBudget: Decimal - ruleBasedBudget: Dict[str, str] + dynamicBidding: Dict[str, Any] startDate: str - endDate: str = None - premiumBidAdjustment: bool - bidding: Bidding - networks: str + endDate: str + budget: Dict[str, Any] + extendedData: Optional[Dict[str, Any]] class ProductAdGroups(CatalogModel): - adGroupId: Decimal + adGroupId: str name: str - campaignId: Decimal + campaignId: str defaultBid: Decimal state: str + extendedData: dict -class SuggestedBid(CatalogModel): - suggested: Decimal - rangeStart: Decimal - rangeEnd: Decimal +class BidRecommendations(CatalogModel): + bidValues: List[Dict[str, str]] + targetingExpression: Dict[str, str] class ProductAdGroupBidRecommendations(CatalogModel): - adGroupId: Decimal - suggestedBid: Optional[SuggestedBid] = None + adGroupId: str + campaignId: str + theme: str + bidRecommendationsForTargetingExpressions: List[BidRecommendations] class SuggestedKeyword(CatalogModel): @@ -60,20 +59,56 @@ class SuggestedKeyword(CatalogModel): class ProductAdGroupSuggestedKeywords(CatalogModel): - adGroupId: Decimal + adGroupId: int suggestedKeywords: List[SuggestedKeyword] = None class ProductAd(CatalogModel): - adId: Decimal - campaignId: Decimal - adGroupId: Decimal - sku: str + adId: str + campaignId: str + customText: str asin: str state: str + sku: str + adGroupId: str + extendedData: Optional[Dict[str, Any]] -class ProductTargeting(Targeting): - campaignId: Decimal +class ProductTargeting(CatalogModel): expression: List[Dict[str, str]] + targetId: str resolvedExpression: List[Dict[str, str]] + campaignId: str + expressionType: str + state: str + bid: float + adGroupId: str + extendedData: Optional[Dict[str, Any]] + + +class SponsoredProductCampaignNegativeKeywordsModel(KeywordsBase): + keywordId: str + campaignId: str + state: str + keywordText: str + extendedData: Optional[Dict[str, Any]] + + +class SponsoredProductKeywordsModel(KeywordsBase): + keywordId: str + nativeLanguageLocale: str + campaignId: str + state: str + adGroupId: str + keywordText: str + extendedData: Optional[Dict[str, Any]] + + +class SponsoredProductNegativeKeywordsModel(KeywordsBase): + keywordId: str + nativeLanguageLocale: str + campaignId: str + state: str + adGroupId: str + keywordText: str + extendedData: Optional[Dict[str, Any]] diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/__init__.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/__init__.py index a449faafedb6..caa39bccfc91 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/__init__.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/__init__.py @@ -16,7 +16,11 @@ SponsoredDisplayReportStream, SponsoredProductsReportStream, ) -from .sponsored_brands import SponsoredBrandsAdGroups, SponsoredBrandsCampaigns, SponsoredBrandsKeywords +from .sponsored_brands import ( + SponsoredBrandsAdGroups, + SponsoredBrandsCampaigns, + SponsoredBrandsKeywords +) from .sponsored_display import ( SponsoredDisplayAdGroups, SponsoredDisplayBudgetRules, diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/attribution_report.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/attribution_report.py index 38d2073e5e2a..4c683846e6d2 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/attribution_report.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/attribution_report.py @@ -82,7 +82,7 @@ def metrics(self): def http_method(self) -> str: return "POST" - def path(self, **kvargs) -> str: + def path(self, **kwargs) -> str: return "/attribution/report" def get_json_schema(self): diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/common.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/common.py index 247122c1e9c2..9ad9bb481f94 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/common.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/common.py @@ -30,14 +30,16 @@ class to provide explanation why it had been done in this way. │ ├── SponsoredDisplayCampaigns │ ├── SponsoredDisplayProductAds │ ├── SponsoredDisplayTargetings - │ ├── SponsoredProductAdGroups - │ ├── SponsoredProductAds - │ ├── SponsoredProductCampaigns - │ ├── SponsoredProductKeywords - │ ├── SponsoredProductNegativeKeywords - │ ├── SponsoredProductTargetings - │ ├── SponsoredBrandsCampaigns - │ ├── SponsoredBrandsAdGroups + │ ├── SponsoredProductsV3 + │ | ├── SponsoredProductAdGroups + │ | ├── SponsoredProductAds + │ | ├── SponsoredProductCampaigns + │ | ├── SponsoredProductKeywords + │ | ├── SponsoredProductNegativeKeywords + │ | └── SponsoredProductTargetings + │ ├── SponsoredBrandsV4 + │ | ├── SponsoredBrandsCampaigns + │ | └── SponsoredBrandsAdGroups │ └── SponsoredBrandsKeywords └── ReportStream ├── SponsoredBrandsReportStream @@ -117,7 +119,7 @@ def raise_on_http_errors(self): def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: return None - def request_headers(self, *args, **kvargs) -> MutableMapping[str, Any]: + def request_headers(self, *args, **kwargs) -> MutableMapping[str, Any]: return {"Amazon-Advertising-API-ClientId": self._client_id} def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: @@ -171,9 +173,9 @@ class SubProfilesStream(AmazonAdsStream): page_size = 100 - def __init__(self, *args, **kvargs): + def __init__(self, *args, **kwargs): self._current_offset = 0 - super().__init__(*args, **kvargs) + super().__init__(*args, **kwargs) def next_page_token(self, response: requests.Response) -> Optional[int]: if not response: @@ -199,15 +201,15 @@ def request_params( "count": self.page_size, } - def read_records(self, *args, **kvargs) -> Iterable[Mapping[str, Any]]: + def read_records(self, *args, **kwargs) -> Iterable[Mapping[str, Any]]: """ Iterate through self._profiles list and send read all records for each profile. """ for profile in self._profiles: self._current_profile_id = profile.profileId - yield from super().read_records(*args, **kvargs) + yield from super().read_records(*args, **kwargs) - def request_headers(self, *args, **kvargs) -> MutableMapping[str, Any]: - headers = super().request_headers(*args, **kvargs) + def request_headers(self, *args, **kwargs) -> MutableMapping[str, Any]: + headers = super().request_headers(*args, **kwargs) headers["Amazon-Advertising-API-Scope"] = str(self._current_profile_id) return headers diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/portfolios.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/portfolios.py index 6892d8ffa896..1d253fda57e6 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/portfolios.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/portfolios.py @@ -17,18 +17,18 @@ class Portfolios(AmazonAdsStream): primary_key = "portfolioId" model = Portfolio - def path(self, **kvargs) -> str: + def path(self, **kwargs) -> str: return "v2/portfolios/extended" - def read_records(self, *args, **kvargs) -> Iterable[Mapping[str, Any]]: + def read_records(self, *args, **kwargs) -> Iterable[Mapping[str, Any]]: """ Iterate through self._profiles list and send read all records for each profile. """ for profile in self._profiles: self._current_profile_id = profile.profileId - yield from super().read_records(*args, **kvargs) + yield from super().read_records(*args, **kwargs) - def request_headers(self, *args, **kvargs) -> MutableMapping[str, Any]: - headers = super().request_headers(*args, **kvargs) + def request_headers(self, *args, **kwargs) -> MutableMapping[str, Any]: + headers = super().request_headers(*args, **kwargs) headers["Amazon-Advertising-API-Scope"] = str(self._current_profile_id) return headers diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/profiles.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/profiles.py index 9663656f3689..c6491c052acf 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/profiles.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/profiles.py @@ -19,7 +19,7 @@ class Profiles(AmazonAdsStream): primary_key = "profileId" model = Profile - def path(self, **kvargs) -> str: + def path(self, **kwargs) -> str: return "v2/profiles?profileTypeFilter=seller,vendor" def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: @@ -30,13 +30,13 @@ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapp self._profiles.append(profile_id_obj) yield record - def read_records(self, *args, **kvargs) -> Iterable[Mapping[str, Any]]: + def read_records(self, *args, **kwargs) -> Iterable[Mapping[str, Any]]: if self._profiles: # In case if we have _profiles populated we can use it instead of making API call. yield from [profile.dict(exclude_unset=True) for profile in self._profiles] else: # Make API call by the means of basic HttpStream class. - yield from super().read_records(*args, **kvargs) + yield from super().read_records(*args, **kwargs) def get_all_profiles(self) -> List[Profile]: """ diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_brands.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_brands.py index 025fb44d3d33..030ae17f94e0 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_brands.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_brands.py @@ -1,15 +1,56 @@ # # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # +from typing import Any, Mapping, MutableMapping +from requests import Response from source_amazon_ads.schemas import BrandsAdGroup, BrandsCampaign from source_amazon_ads.streams.common import SubProfilesStream -class SponsoredBrandsCampaigns(SubProfilesStream): +class SponsoredBrandsV4(SubProfilesStream): """ - This stream corresponds to Amazon Advertising API - Sponsored Brands Campaigns - https://advertising.amazon.com/API/docs/en-us/sponsored-brands/3-0/openapi#/Campaigns + This Stream supports the Sponsored Brands V4 API, which requires POST methods + https://advertising.amazon.com/API/docs/en-us/sponsored-brands/3-0/openapi/prod + """ + + @property + def http_method(self, **kwargs) -> str: + return "POST" + + def request_headers(self, profile_id: str = None, *args, **kwargs) -> MutableMapping[str, Any]: + headers = super().request_headers(*args, **kwargs) + headers["Accept"] = self.content_type + headers["Content-Type"] = self.content_type + return headers + + def request_body_json( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + request_body = {} + request_body["maxResults"] = self.page_size + if next_page_token: + request_body["nextToken"] = next_page_token + return request_body + + def next_page_token(self, response: Response) -> str: + if not response: + return None + return response.json().get("nextToken", None) + + def request_params( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: int = None, + ) -> MutableMapping[str, Any]: + return {} + + +class SponsoredBrandsCampaigns(SponsoredBrandsV4): + """ + This stream corresponds to Amazon Ads API - Sponsored Brands Campaigns v4 + https://advertising.amazon.com/API/docs/en-us/sponsored-brands/3-0/openapi/prod#tag/Campaigns/operation/ListSponsoredBrandsCampaigns """ def __init__(self, *args, **kwargs): @@ -17,30 +58,36 @@ def __init__(self, *args, **kwargs): self.state_filter = kwargs.get("config", {}).get("state_filter") primary_key = "campaignId" + data_field = "campaigns" state_filter = None + content_type = "application/vnd.sbcampaignresource.v4+json" model = BrandsCampaign - def path(self, **kvargs) -> str: - return "sb/campaigns" + def path(self, **kwargs) -> str: + return "sb/v4/campaigns/list" - def request_params(self, *args, **kwargs): - params = super().request_params(*args, **kwargs) + def request_body_json( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + request_body = super().request_body_json(stream_state, stream_slice, next_page_token) if self.state_filter: - params["stateFilter"] = ",".join(self.state_filter) - return params + request_body["stateFilter"] = {"include": self.state_filter} + return request_body -class SponsoredBrandsAdGroups(SubProfilesStream): +class SponsoredBrandsAdGroups(SponsoredBrandsV4): """ - This stream corresponds to Amazon Advertising API - Sponsored Brands Ad groups - https://advertising.amazon.com/API/docs/en-us/sponsored-brands/3-0/openapi#/Ad%20groups + This stream corresponds to Amazon Ads API - Sponsored Brands Ad Groups v4 + https://advertising.amazon.com/API/docs/en-us/sponsored-brands/3-0/openapi/prod#tag/Ad-groups/operation/ListSponsoredBrandsAdGroups """ primary_key = "adGroupId" + data_field = "adGroups" model = BrandsAdGroup + content_type = "application/vnd.sbadgroupresource.v4+json" - def path(self, **kvargs) -> str: - return "sb/adGroups" + def path(self, **kwargs) -> str: + return "sb/v4/adGroups/list" class SponsoredBrandsKeywords(SubProfilesStream): @@ -52,5 +99,5 @@ class SponsoredBrandsKeywords(SubProfilesStream): primary_key = "adGroupId" model = BrandsAdGroup - def path(self, **kvargs) -> str: + def path(self, **kwargs) -> str: return "sb/keywords" diff --git a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_products.py b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_products.py index c3b596ae0490..3b465e9ce2b6 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_products.py +++ b/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/streams/sponsored_products.py @@ -2,29 +2,70 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # +import json from abc import ABC from http import HTTPStatus from typing import Any, Iterable, List, Mapping, MutableMapping, Optional -import requests as requests from airbyte_protocol.models import SyncMode +from requests import Response from source_amazon_ads.schemas import ( - Keywords, - NegativeKeywords, ProductAd, ProductAdGroupBidRecommendations, ProductAdGroups, ProductAdGroupSuggestedKeywords, ProductCampaign, ProductTargeting, + SponsoredProductCampaignNegativeKeywordsModel, + SponsoredProductKeywordsModel, + SponsoredProductNegativeKeywordsModel, ) -from source_amazon_ads.streams.common import AmazonAdsStream, SubProfilesStream +from source_amazon_ads.streams.common import SubProfilesStream -class SponsoredProductCampaigns(SubProfilesStream): +class SponsoredProductsV3(SubProfilesStream): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Campaigns - https://advertising.amazon.com/API/docs/en-us/sponsored-display/3-0/openapi#/Campaigns + This Stream supports the Sponsored Products v3 API, which requires POST methods + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod + """ + + @property + def http_method(self, **kwargs) -> str: + return "POST" + + def request_headers(self, profile_id: str = None, *args, **kwargs) -> MutableMapping[str, Any]: + headers = super().request_headers(*args, **kwargs) + headers["Accept"] = self.content_type + headers["Content-Type"] = self.content_type + return headers + + def next_page_token(self, response: Response) -> str: + if not response: + return None + return response.json().get("nextToken", None) + + def request_body_json( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + request_body = {} + request_body["maxResults"] = self.page_size + if next_page_token: + request_body["nextToken"] = next_page_token + return request_body + + def request_params( + self, + stream_state: Mapping[str, Any], + stream_slice: Mapping[str, Any] = None, + next_page_token: int = None, + ) -> MutableMapping[str, Any]: + return {} + + +class SponsoredProductCampaigns(SponsoredProductsV3): + """ + This stream corresponds to Amazon Ads API - Sponsored Products (v3) Campaigns + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#tag/Campaigns/operation/ListSponsoredProductsCampaigns """ def __init__(self, *args, **kwargs): @@ -32,42 +73,39 @@ def __init__(self, *args, **kwargs): self.state_filter = kwargs.get("config", {}).get("state_filter") primary_key = "campaignId" + data_field = "campaigns" state_filter = None model = ProductCampaign + content_type = "application/vnd.spCampaign.v3+json" - def path(self, **kvargs) -> str: - return "v2/sp/campaigns" + def path(self, **kwargs) -> str: + return "sp/campaigns/list" - def request_params(self, *args, **kwargs): - params = super().request_params(*args, **kwargs) + def request_body_json( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + request_body = super().request_body_json(stream_state, stream_slice, next_page_token) if self.state_filter: - params["stateFilter"] = ",".join(self.state_filter) - return params + request_body["stateFilter"] = {"include": self.state_filter} + return request_body -class SponsoredProductAdGroups(SubProfilesStream): +class SponsoredProductAdGroups(SponsoredProductsV3): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Ad groups - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Ad%20groups + This stream corresponds to Amazon Ads API - Sponsored Products (v3) Ad groups + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#tag/Ad-groups/operation/ListSponsoredProductsAdGroups """ primary_key = "adGroupId" + data_field = "adGroups" + content_type = "application/vnd.spAdGroup.v3+json" model = ProductAdGroups - def path(self, **kvargs) -> str: - return "v2/sp/adGroups" - + def path(self, **kwargs) -> str: + return "/sp/adGroups/list" -class SponsoredProductAdGroupsWithProfileId(SponsoredProductAdGroups): - """Add profileId attr for each records in SponsoredProductAdGroups stream""" - def parse_response(self, *args, **kwargs) -> Iterable[Mapping]: - for record in super().parse_response(*args, **kwargs): - record["profileId"] = self._current_profile_id - yield record - - -class SponsoredProductAdGroupWithSlicesABC(AmazonAdsStream, ABC): +class SponsoredProductAdGroupWithSlicesABC(SponsoredProductsV3, ABC): """ABC Class for extraction of additional information for each known sp ad group""" primary_key = "adGroupId" @@ -77,19 +115,14 @@ def __init__(self, *args, **kwargs): self.__kwargs = kwargs super().__init__(*args, **kwargs) - def request_headers(self, *args, **kvargs) -> MutableMapping[str, Any]: - headers = super().request_headers(*args, **kvargs) - headers["Amazon-Advertising-API-Scope"] = str(kvargs["stream_slice"]["profileId"]) - return headers - def stream_slices( self, *, sync_mode: SyncMode, cursor_field: List[str] = None, stream_state: Mapping[str, Any] = None ) -> Iterable[Optional[Mapping[str, Any]]]: - yield from SponsoredProductAdGroupsWithProfileId(*self.__args, **self.__kwargs).read_records( + yield from SponsoredProductAdGroups(*self.__args, **self.__kwargs).read_records( sync_mode=sync_mode, cursor_field=cursor_field, stream_slice=None, stream_state=stream_state ) - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: + def parse_response(self, response: Response, **kwargs) -> Iterable[Mapping]: resp = response.json() if response.status_code == HTTPStatus.OK: @@ -104,6 +137,12 @@ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapp f"Skip current AdGroup because it does not support request {response.request.url} for " f"{response.request.headers['Amazon-Advertising-API-Scope']} profile: {response.text}" ) + elif response.status_code == HTTPStatus.UNPROCESSABLE_ENTITY: + # 422 error message for bids recommendation: + # No recommendations can be provided as the input ad group does not have any asins. + self.logger.warning( + f"Skip current AdGroup because the ad group {json.loads(response.request.body)['adGroupId']} does not have any asins {response.request.url}" + ) elif response.status_code == HTTPStatus.NOT_FOUND: # 404 Either the specified ad group identifier was not found, # or the specified ad group was found but no associated bid was found. @@ -117,98 +156,148 @@ def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapp class SponsoredProductAdGroupBidRecommendations(SponsoredProductAdGroupWithSlicesABC): - """Docs: - Latest API: - https://advertising.amazon.com/API/docs/en-us/sponsored-display/3-0/openapi#/Bid%20Recommendations/getTargetBidRecommendations - POST /sd/targets/bid/recommendations - Note: does not work, always get "403 Forbidden" - - V2 API: - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Bid%20recommendations/getAdGroupBidRecommendations - GET /v2/sp/adGroups/{adGroupId}/bidRecommendations + """ + This stream corresponds to Amazon Ads API - Sponsored Products (v3) Ad group bid recommendations, now referred to as "Target Bid Recommendations" by Amazon Ads + https://advertising.amazon.com/API/docs/en-us/sponsored-display/3-0/openapi#tag/Bid-Recommendations/operation/getTargetBidRecommendations """ + primary_key = None + data_field = "bidRecommendations" + content_type = "application/vnd.spthemebasedbidrecommendation.v4+json" model = ProductAdGroupBidRecommendations def path(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> str: - return f"v2/sp/adGroups/{stream_slice['adGroupId']}/bidRecommendations" + return "/sp/targets/bid/recommendations" + + def request_body_json( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + self.current_ad_group_id = stream_slice["adGroupId"] + self.current_campaign_id = stream_slice["campaignId"] + + request_body = {} + request_body["targetingExpressions"] = [ + {"type": "CLOSE_MATCH"}, + {"type": "LOOSE_MATCH"}, + {"type": "SUBSTITUTES"}, + {"type": "COMPLEMENTS"}, + ] + request_body["adGroupId"] = stream_slice["adGroupId"] + request_body["campaignId"] = stream_slice["campaignId"] + request_body["recommendationType"] = "BIDS_FOR_EXISTING_AD_GROUP" + return request_body + + def parse_response(self, response: Response, **kwargs) -> Iterable[Mapping]: + for record in super().parse_response(response, **kwargs): + record["adGroupId"] = self.current_ad_group_id + record["campaignId"] = self.current_campaign_id + yield record class SponsoredProductAdGroupSuggestedKeywords(SponsoredProductAdGroupWithSlicesABC): """Docs: - Latest API: - https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#/Keyword%20Targets/getRankedKeywordRecommendation - POST /sp/targets/keywords/recommendations - Note: does not work, always get "403 Forbidden" - V2 API: https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Suggested%20keywords GET /v2/sp/adGroups/{{adGroupId}}>/suggested/keywords """ + primary_key = None + data_field = "" model = ProductAdGroupSuggestedKeywords + @property + def http_method(self, **kwargs) -> str: + return "GET" + def path(self, stream_slice: Mapping[str, Any] = None, **kwargs) -> str: return f"v2/sp/adGroups/{stream_slice['adGroupId']}/suggested/keywords" + def request_params( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: int = None + ) -> MutableMapping[str, Any]: + return {"maxNumSuggestions": 100} -class SponsoredProductKeywords(SubProfilesStream): + def request_headers(self, profile_id: str = None, *args, **kwargs) -> MutableMapping[str, Any]: + headers = {} + headers["Amazon-Advertising-API-Scope"] = str(self._current_profile_id) + headers["Amazon-Advertising-API-ClientId"] = self._client_id + return headers + + def request_body_json( + self, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, next_page_token: Mapping[str, Any] = None + ) -> Mapping[str, Any]: + return {} + + +class SponsoredProductKeywords(SponsoredProductsV3): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Keywords - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Keywords + This stream corresponds to Amazon Ads Sponsored Products v3 API - Sponsored Products Keywords + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#tag/Keywords/operation/ListSponsoredProductsKeywords """ primary_key = "keywordId" - model = Keywords + data_field = "keywords" + content_type = "application/vnd.spKeyword.v3+json" + model = SponsoredProductKeywordsModel - def path(self, **kvargs) -> str: - return "v2/sp/keywords" + def path(self, **kwargs) -> str: + return "sp/keywords/list" -class SponsoredProductNegativeKeywords(SubProfilesStream): +class SponsoredProductNegativeKeywords(SponsoredProductsV3): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Negative Keywords - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Negative%20keywords + This stream corresponds to Amazon Ads Sponsored Products v3 API - Sponsored Products Negative Keywords + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#tag/Negative-keywords/operation/ListSponsoredProductsNegativeKeywords """ primary_key = "keywordId" - model = NegativeKeywords + data_field = "negativeKeywords" + content_type = "application/vnd.spNegativeKeyword.v3+json" + model = SponsoredProductNegativeKeywordsModel - def path(self, **kvargs) -> str: - return "v2/sp/negativeKeywords" + def path(self, **kwargs) -> str: + return "sp/negativeKeywords/list" -class SponsoredProductCampaignNegativeKeywords(SponsoredProductNegativeKeywords): +class SponsoredProductCampaignNegativeKeywords(SponsoredProductsV3): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Negative Keywords - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Negative%20keywords + This stream corresponds to Amazon Ads Sponsored Products v3 API - Sponsored Products Negative Keywords + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#tag/Campaign-negative-keywords/operation/ListSponsoredProductsCampaignNegativeKeywords """ - def path(self, **kvargs) -> str: - return "v2/sp/campaignNegativeKeywords" + primary_key = "keywordId" + data_field = "campaignNegativeKeywords" + content_type = "application/vnd.spCampaignNegativeKeyword.v3+json" + model = SponsoredProductCampaignNegativeKeywordsModel + + def path(self, **kwargs) -> str: + return "sp/campaignNegativeKeywords/list" -class SponsoredProductAds(SubProfilesStream): +class SponsoredProductAds(SponsoredProductsV3): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Ads - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Product%20ads + This stream corresponds to Amazon Ads v3 API - Sponsored Products Ads + https://advertising.amazon.com/API/docs/en-us/sponsored-products/3-0/openapi/prod#tag/Product-ads/operation/ListSponsoredProductsProductAds """ primary_key = "adId" + data_field = "productAds" + content_type = "application/vnd.spProductAd.v3+json" model = ProductAd - def path(self, **kvargs) -> str: - return "v2/sp/productAds" + def path(self, **kwargs) -> str: + return "sp/productAds/list" -class SponsoredProductTargetings(SubProfilesStream): +class SponsoredProductTargetings(SponsoredProductsV3): """ - This stream corresponds to Amazon Advertising API - Sponsored Products Targetings - https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Product%20targeting + This stream corresponds to Amazon Ads Sponsored Products v3 API - Sponsored Products Targeting Clauses """ primary_key = "targetId" + data_field = "targetingClauses" + content_type = "application/vnd.spTargetingClause.v3+json" model = ProductTargeting - def path(self, **kvargs) -> str: - return "v2/sp/targets" + def path(self, **kwargs) -> str: + return "sp/targets/list" diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_requests/sponsored_brands_request_builder.py b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_requests/sponsored_brands_request_builder.py index 400fe018e55f..65b947483c66 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_requests/sponsored_brands_request_builder.py +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_requests/sponsored_brands_request_builder.py @@ -8,14 +8,12 @@ class SponsoredBrandsRequestBuilder(AmazonAdsBaseRequestBuilder): @classmethod def ad_groups_endpoint( - cls, client_id: str, client_access_token: str, profile_id: str, limit: Optional[int] = 100, start_index: Optional[int] = 0 + cls, client_id: str, client_access_token: str, profile_id: str ) -> "SponsoredBrandsRequestBuilder": - return cls("sb/adGroups") \ + return cls("sb/v4/adGroups/list") \ .with_client_id(client_id) \ .with_client_access_token(client_access_token) \ - .with_profile_id(profile_id) \ - .with_limit(limit) \ - .with_start_index(start_index) + .with_profile_id(profile_id) @classmethod def keywords_endpoint( @@ -30,19 +28,18 @@ def keywords_endpoint( @classmethod def campaigns_endpoint( - cls, client_id: str, client_access_token: str, profile_id: str, limit: Optional[int] = 100, start_index: Optional[int] = 0 + cls, client_id: str, client_access_token: str, profile_id: str ) -> "SponsoredBrandsRequestBuilder": - return cls("sb/campaigns") \ + return cls("sb/v4/campaigns/list") \ .with_client_id(client_id) \ .with_client_access_token(client_access_token) \ - .with_profile_id(profile_id) \ - .with_limit(limit) \ - .with_start_index(start_index) + .with_profile_id(profile_id) def __init__(self, resource: str) -> None: super().__init__(resource) self._limit: Optional[int] = None self._start_index: Optional[int] = None + self._body: dict = None @property def query_params(self) -> Dict[str, Any]: @@ -55,7 +52,7 @@ def query_params(self) -> Dict[str, Any]: @property def request_body(self) ->Optional[str]: - return None + return self._body def with_limit(self, limit: int) -> "SponsoredBrandsRequestBuilder": self._limit: int = limit @@ -64,3 +61,7 @@ def with_limit(self, limit: int) -> "SponsoredBrandsRequestBuilder": def with_start_index(self, offset: int) -> "SponsoredBrandsRequestBuilder": self._start_index: int = offset return self + + def with_request_body(self, body: dict) -> "SponsoredBrandsRequestBuilder": + self._body: dict = body + return self diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/__init__.py b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/__init__.py index b7884e21612c..692ce0bff949 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/__init__.py +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/__init__.py @@ -1,2 +1,3 @@ from .count_based_pagination_strategy import CountBasedPaginationStrategy from .cursor_based_pagination_strategy import CursorBasedPaginationStrategy +from .sponsored_cursor_based_pagination_strategy import SponsoredCursorBasedPaginationStrategy diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/sponsored_cursor_based_pagination_strategy.py b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/sponsored_cursor_based_pagination_strategy.py new file mode 100644 index 000000000000..04501edbb36f --- /dev/null +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/ad_responses/pagination_strategies/sponsored_cursor_based_pagination_strategy.py @@ -0,0 +1,11 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + +from typing import Any, Dict + +from airbyte_cdk.test.mock_http.response_builder import PaginationStrategy + + +class SponsoredCursorBasedPaginationStrategy(PaginationStrategy): + @staticmethod + def update(response: Dict[str, Any]) -> None: + response["nextToken"] = "next-page-token" diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/test_sponsored_streams.py b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/test_sponsored_streams.py index ffad23c72024..e5096bcb1351 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/test_sponsored_streams.py +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/integrations/test_sponsored_streams.py @@ -1,19 +1,48 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. +import json from unittest import TestCase from unittest.mock import patch from airbyte_cdk.test.mock_http import HttpMocker +from airbyte_cdk.test.mock_http.response_builder import ( + FieldPath, + HttpResponseBuilder, + NestedPath, + PaginationStrategy, + RecordBuilder, + create_record_builder, + create_response_builder, + find_template, +) from airbyte_protocol.models import Level as LogLevel from airbyte_protocol.models import SyncMode from .ad_requests import OAuthRequestBuilder, ProfilesRequestBuilder, SponsoredBrandsRequestBuilder from .ad_responses import ErrorResponseBuilder, OAuthResponseBuilder, ProfilesResponseBuilder, SponsoredBrandsResponseBuilder -from .ad_responses.pagination_strategies import CountBasedPaginationStrategy +from .ad_responses.pagination_strategies import CountBasedPaginationStrategy, SponsoredCursorBasedPaginationStrategy from .ad_responses.records import ErrorRecordBuilder, ProfilesRecordBuilder, SponsoredBrandsRecordBuilder from .config import ConfigBuilder from .utils import get_log_messages_by_log_level, read_stream +_DEFAULT_REQUEST_BODY = json.dumps({ + "maxResults": 100 +}) + +def _a_record(stream_name: str, data_field: str, record_id_path: str) -> RecordBuilder: + return create_record_builder( + find_template(stream_name, __file__), + FieldPath(data_field), + record_id_path=FieldPath(record_id_path), + record_cursor_path=None + ) + +def _a_response(stream_name: str, data_field: str, pagination_strategy: PaginationStrategy = None) -> HttpResponseBuilder: + return create_response_builder( + find_template(stream_name, __file__), + FieldPath(data_field), + pagination_strategy=pagination_strategy + ) class TestSponsoredBrandsStreamsFullRefresh(TestCase): @property @@ -34,7 +63,7 @@ def _given_oauth_and_profiles(self, http_mocker: HttpMocker, config: dict) -> No ) @HttpMocker() - def test_given_non_breaking_error_when_read_ad_groups_then_stream_is_ignored(self, http_mocker): + def test_given_non_breaking_error_when_read_ad_groups_then_stream_is_ignored(self, http_mocker: HttpMocker): """ Check ad groups stream: non-breaking errors are ignored When error of this kind happen, we warn and then keep syncing another streams @@ -42,8 +71,8 @@ def test_given_non_breaking_error_when_read_ad_groups_then_stream_is_ignored(sel self._given_oauth_and_profiles(http_mocker, self._config) non_breaking_error = ErrorRecordBuilder.non_breaking_error() - http_mocker.get( - SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), + http_mocker.post( + SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), ErrorResponseBuilder.non_breaking_error_response().with_record(non_breaking_error).with_status_code(400).build() ) output = read_stream("sponsored_brands_ad_groups", SyncMode.full_refresh, self._config) @@ -53,15 +82,15 @@ def test_given_non_breaking_error_when_read_ad_groups_then_stream_is_ignored(sel assert any([non_breaking_error.build().get("details") in worning for worning in warning_logs]) @HttpMocker() - def test_given_breaking_error_when_read_ad_groups_then_stream_stop_syncing(self, http_mocker): + def test_given_breaking_error_when_read_ad_groups_then_stream_stop_syncing(self, http_mocker: HttpMocker): """ Check ad groups stream: when unknown error happen we stop syncing with raising the error """ self._given_oauth_and_profiles(http_mocker, self._config) breaking_error = ErrorRecordBuilder.breaking_error() - http_mocker.get( - SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), + http_mocker.post( + SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), ErrorResponseBuilder.breaking_error_response().with_record(breaking_error).with_status_code(500).build() ) with patch('time.sleep', return_value=None): @@ -72,45 +101,57 @@ def test_given_breaking_error_when_read_ad_groups_then_stream_stop_syncing(self, assert any([breaking_error.build().get("message") in error for error in error_logs]) @HttpMocker() - def test_given_one_page_when_read_ad_groups_then_return_records(self, http_mocker): + def test_given_one_page_when_read_ad_groups_then_return_records(self, http_mocker: HttpMocker): """ Check ad groups stream: normal full refresh sync without pagination """ + stream_name = "sponsored_brands_ad_groups" + data_field = "adGroups" + record_id_path = "adGroupId" + self._given_oauth_and_profiles(http_mocker, self._config) - http_mocker.get( - SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), - SponsoredBrandsResponseBuilder.ad_groups_response().with_record(SponsoredBrandsRecordBuilder.ad_groups_record()).build() + http_mocker.post( + SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), + _a_response(stream_name, data_field, None).with_record(_a_record(stream_name, data_field, record_id_path)).build() ) output = read_stream("sponsored_brands_ad_groups", SyncMode.full_refresh, self._config) + print(output.records) assert len(output.records) == 1 @HttpMocker() - def test_given_many_pages_when_read_ad_groups_then_return_records(self, http_mocker): + def test_given_many_pages_when_read_ad_groups_then_return_records(self, http_mocker: HttpMocker): """ Check ad groups stream: normal full refresh sync with pagination """ + + stream_name = "sponsored_brands_ad_groups" + data_field = "adGroups" + record_id_path = "adGroupId" + pagination_strategy = SponsoredCursorBasedPaginationStrategy() + + paginated_request_body = json.dumps({ + "maxResults": 100, + "nextToken": "next-page-token" + }) + self._given_oauth_and_profiles(http_mocker, self._config) - http_mocker.get( - SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), - SponsoredBrandsResponseBuilder.ad_groups_response(CountBasedPaginationStrategy()).with_record(SponsoredBrandsRecordBuilder.ad_groups_record()).with_pagination().build() - ) - http_mocker.get( - SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100, start_index=100).build(), - SponsoredBrandsResponseBuilder.ad_groups_response(CountBasedPaginationStrategy()).with_record(SponsoredBrandsRecordBuilder.ad_groups_record()).with_pagination().build() + http_mocker.post( + SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), + _a_response(stream_name, data_field, pagination_strategy).with_record(_a_record(stream_name, data_field, record_id_path)).with_pagination().build() ) - http_mocker.get( - SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100, start_index=200).build(), - SponsoredBrandsResponseBuilder.ad_groups_response().with_record(SponsoredBrandsRecordBuilder.ad_groups_record()).build() + http_mocker.post( + SponsoredBrandsRequestBuilder.ad_groups_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(paginated_request_body).build(), + _a_response(stream_name, data_field, pagination_strategy).with_record(_a_record(stream_name, data_field, record_id_path)).build() ) output = read_stream("sponsored_brands_ad_groups", SyncMode.full_refresh, self._config) - assert len(output.records) == 201 + assert len(output.records) == 2 @HttpMocker() - def test_given_non_breaking_error_when_read_campaigns_then_stream_is_ignored(self, http_mocker): + def test_given_non_breaking_error_when_read_campaigns_then_stream_is_ignored(self, http_mocker: HttpMocker): """ Check campaigns stream: non-breaking errors are ignored When error of this kind happen, we warn and then keep syncing another streams @@ -118,8 +159,8 @@ def test_given_non_breaking_error_when_read_campaigns_then_stream_is_ignored(sel self._given_oauth_and_profiles(http_mocker, self._config) non_breaking_error = ErrorRecordBuilder.non_breaking_error() - http_mocker.get( - SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), + http_mocker.post( + SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), ErrorResponseBuilder.non_breaking_error_response().with_record(non_breaking_error).with_status_code(400).build() ) output = read_stream("sponsored_brands_campaigns", SyncMode.full_refresh, self._config) @@ -129,15 +170,15 @@ def test_given_non_breaking_error_when_read_campaigns_then_stream_is_ignored(sel assert any([non_breaking_error.build().get("details") in worning for worning in warning_logs]) @HttpMocker() - def test_given_breaking_error_when_read_campaigns_then_stream_stop_syncing(self, http_mocker): + def test_given_breaking_error_when_read_campaigns_then_stream_stop_syncing(self, http_mocker: HttpMocker): """ Check campaigns stream: when unknown error happen we stop syncing with raising the error """ self._given_oauth_and_profiles(http_mocker, self._config) breaking_error = ErrorRecordBuilder.breaking_error() - http_mocker.get( - SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), + http_mocker.post( + SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), ErrorResponseBuilder.breaking_error_response().with_record(breaking_error).with_status_code(500).build() ) with patch('time.sleep', return_value=None): @@ -148,45 +189,56 @@ def test_given_breaking_error_when_read_campaigns_then_stream_stop_syncing(self, assert any([breaking_error.build().get("message") in error for error in error_logs]) @HttpMocker() - def test_given_one_page_when_read_campaigns_then_return_records(self, http_mocker): + def test_given_one_page_when_read_campaigns_then_return_records(self, http_mocker: HttpMocker): """ Check campaigns stream: normal full refresh sync without pagination """ self._given_oauth_and_profiles(http_mocker, self._config) - http_mocker.get( - SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), - SponsoredBrandsResponseBuilder.campaigns_response().with_record(SponsoredBrandsRecordBuilder.campaigns_record()).build() + stream_name = "sponsored_brands_campaigns" + data_field = "campaigns" + record_id_path = "campaignId" + + http_mocker.post( + SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), + _a_response(stream_name, data_field, None).with_record(_a_record(stream_name, data_field, record_id_path)).build() ) output = read_stream("sponsored_brands_campaigns", SyncMode.full_refresh, self._config) assert len(output.records) == 1 @HttpMocker() - def test_given_many_pages_when_read_campaigns_then_return_records(self, http_mocker): + def test_given_many_pages_when_read_campaigns_then_return_records(self, http_mocker: HttpMocker): """ Check campaigns stream: normal full refresh sync with pagination """ + + stream_name = "sponsored_brands_campaigns" + data_field = "campaigns" + record_id_path = "campaignId" + pagination_strategy = SponsoredCursorBasedPaginationStrategy() + + paginated_request_body = json.dumps({ + "maxResults": 100, + "nextToken": "next-page-token" + }) + self._given_oauth_and_profiles(http_mocker, self._config) - http_mocker.get( - SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100).build(), - SponsoredBrandsResponseBuilder.campaigns_response(CountBasedPaginationStrategy()).with_record(SponsoredBrandsRecordBuilder.campaigns_record()).with_pagination().build() - ) - http_mocker.get( - SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100, start_index=100).build(), - SponsoredBrandsResponseBuilder.campaigns_response(CountBasedPaginationStrategy()).with_record(SponsoredBrandsRecordBuilder.campaigns_record()).with_pagination().build() + http_mocker.post( + SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(_DEFAULT_REQUEST_BODY).build(), + _a_response(stream_name, data_field, pagination_strategy).with_record(_a_record(stream_name, data_field, record_id_path)).with_pagination().build() ) - http_mocker.get( - SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0], limit=100, start_index=200).build(), - SponsoredBrandsResponseBuilder.campaigns_response().with_record(SponsoredBrandsRecordBuilder.campaigns_record()).build() + http_mocker.post( + SponsoredBrandsRequestBuilder.campaigns_endpoint(self._config["client_id"], self._config["access_token"], self._config["profiles"][0]).with_request_body(paginated_request_body).build(), + _a_response(stream_name, data_field, pagination_strategy).with_record(_a_record(stream_name, data_field, record_id_path)).build() ) output = read_stream("sponsored_brands_campaigns", SyncMode.full_refresh, self._config) - assert len(output.records) == 201 + assert len(output.records) == 2 @HttpMocker() - def test_given_non_breaking_error_when_read_keywords_then_stream_is_ignored(self, http_mocker): + def test_given_non_breaking_error_when_read_keywords_then_stream_is_ignored(self, http_mocker: HttpMocker): """ Check keywords stream: non-breaking errors are ignored When error of this kind happen, we warn and then keep syncing another streams @@ -205,7 +257,7 @@ def test_given_non_breaking_error_when_read_keywords_then_stream_is_ignored(self assert any([non_breaking_error.build().get("details") in worning for worning in warning_logs]) @HttpMocker() - def test_given_breaking_error_when_read_keywords_then_stream_stop_syncing(self, http_mocker): + def test_given_breaking_error_when_read_keywords_then_stream_stop_syncing(self, http_mocker: HttpMocker): """ Check keywords stream: when unknown error happen we stop syncing with raising the error """ @@ -224,7 +276,7 @@ def test_given_breaking_error_when_read_keywords_then_stream_stop_syncing(self, assert any([breaking_error.build().get("message") in error for error in error_logs]) @HttpMocker() - def test_given_one_page_when_read_keywords_then_return_records(self, http_mocker): + def test_given_one_page_when_read_keywords_then_return_records(self, http_mocker: HttpMocker): """ Check keywords stream: normal full refresh sync without pagination """ @@ -239,7 +291,7 @@ def test_given_one_page_when_read_keywords_then_return_records(self, http_mocker assert len(output.records) == 1 @HttpMocker() - def test_given_many_pages_when_read_keywords_then_return_records(self, http_mocker): + def test_given_many_pages_when_read_keywords_then_return_records(self, http_mocker: HttpMocker): """ Check keywords stream: normal full refresh sync with pagination """ diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_ad_groups.json b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_ad_groups.json index e7ef472e3783..35f0f155282c 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_ad_groups.json +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_ad_groups.json @@ -1,7 +1,12 @@ -[ - { - "campaignId": 1, - "adGroupId": 1, - "name": "string" - } -] +{ + "adGroups": [ + { + "campaignId": "string", + "name": "string", + "state": "ENABLED", + "adGroupId": "string", + "extendedData": {} + } + ], + "totalResults": 100 +} diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_campaigns.json b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_campaigns.json index 51b88a4d0895..07abeb5404d3 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_campaigns.json +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/resource/http/response/sponsored_brands_campaigns.json @@ -1,42 +1,53 @@ -[ - { - "campaignId": 1, - "name": "string", - "budget": 0, - "budgetType": "lifetime", - "startDate": "string", - "endDate": "string", - "state": "enabled", - "servingStatus": "asinNotBuyable", - "portfolioId": 0, - "bidOptimization": true, - "bidMultiplier": 0, - "bidAdjustments": [ - { - "bidAdjustmentPredicate": "placementGroupHome", - "bidAdjustmentPercent": 50 +{ + "campaigns": [ + { + "budgetType": "DAILY", + "ruleBasedBudget": { + "isProcessing": true, + "applicableRuleName": "string", + "value": 0.1, + "applicableRuleId": "string" }, - { - "bidAdjustmentPredicate": "placementGroupDetailPage", - "bidAdjustmentPercent": 50 + "brandEntityId": "string", + "isMultiAdGroupsEnabled": true, + "goal": "string", + "bidding": { + "bidOptimization": true, + "bidAdjustmentsByShopperSegment": [ + { + "percentage": 900, + "shopperSegment": "NEW_TO_BRAND_PURCHASE" + } + ], + "bidAdjustmentsByPlacement": [ + { + "percentage": -99, + "placement": "HOME" + } + ], + "bidOptimizationStrategy": "MAXIMIZE_IMMEDIATE_SALES" }, - { - "bidAdjustmentPredicate": "placementGroupOther", - "bidAdjustmentPercent": 50 + "endDate": "string", + "campaignId": "string", + "productLocation": "SOLD_ON_AMAZON", + "tags": { + "property1": "string", + "property2": "string" + }, + "portfolioId": "string", + "costType": "string", + "smartDefault": ["string"], + "name": "string", + "state": "ENABLED", + "startDate": "string", + "budget": 0.1, + "extendedData": { + "servingStatus": "ADVERTISER_STATUS_ENABLED", + "lastUpdateDate": 0, + "servingStatusDetails": ["string"], + "creationDate": 0 } - ], - "adFormat": "productCollection", - "creative": { - "brandName": "string", - "brandLogoAssetID": "string", - "brandLogoUrl": "string", - "headline": "string", - "asins": ["string"], - "shouldOptimizeAsins": false - }, - "landingPage": { - "pageType": "productList", - "url": "string" } - } -] + ], + "totalCount": 100 +} diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_report_streams.py b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_report_streams.py index 13783837a56a..45c48b39d091 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_report_streams.py +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_report_streams.py @@ -708,50 +708,66 @@ def test_read_incremental_with_records_start_date(config): [ ( ["enabled", "archived", "paused"], - SponsoredBrandsCampaigns, + SponsoredDisplayCampaigns, ), ( ["enabled"], - SponsoredBrandsCampaigns, + SponsoredDisplayCampaigns, ), ( None, - SponsoredBrandsCampaigns, + SponsoredDisplayCampaigns, ), + ], +) +def test_streams_state_filter(mocker, config, state_filter, stream_class): + profiles = make_profiles() + mocker.patch.object(stream_class, "state_filter", new_callable=mocker.PropertyMock, return_value=state_filter) + + stream = stream_class(config, profiles) + params = stream.request_params(stream_state=None, stream_slice=None, next_page_token=None) + if "stateFilter" in params: + assert params["stateFilter"] == ",".join(state_filter) + else: + assert state_filter is None + +@pytest.mark.parametrize( + "state_filter, stream_class", + [ ( ["enabled", "archived", "paused"], - SponsoredProductCampaigns, + SponsoredBrandsCampaigns, ), ( ["enabled"], - SponsoredProductCampaigns, + SponsoredBrandsCampaigns, ), ( None, - SponsoredProductCampaigns, + SponsoredBrandsCampaigns, ), ( ["enabled", "archived", "paused"], - SponsoredDisplayCampaigns, + SponsoredProductCampaigns, ), ( ["enabled"], - SponsoredDisplayCampaigns, + SponsoredProductCampaigns, ), ( None, - SponsoredDisplayCampaigns, + SponsoredProductCampaigns, ), ], ) -def test_streams_state_filter(mocker, config, state_filter, stream_class): +def test_sponsored_brand_and_products_streams_state_filter(mocker, config, state_filter, stream_class): profiles = make_profiles() mocker.patch.object(stream_class, "state_filter", new_callable=mocker.PropertyMock, return_value=state_filter) stream = stream_class(config, profiles) - params = stream.request_params(stream_state=None, stream_slice=None, next_page_token=None) - if "stateFilter" in params: - assert params["stateFilter"] == ",".join(state_filter) + request_body = stream.request_body_json(stream_state=None, stream_slice=None, next_page_token=None) + if "stateFilter" in request_body: + assert request_body["stateFilter"]["include"] == state_filter else: assert state_filter is None diff --git a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_streams.py b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_streams.py index 1eb1a45d3ac1..31264c021008 100644 --- a/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_streams.py +++ b/airbyte-integrations/connectors/source-amazon-ads/unit_tests/test_streams.py @@ -23,6 +23,7 @@ def setup_responses( product_ads_response=None, generic_response=None, creatives_response=None, + post_response=None, ): responses.add( responses.POST, @@ -77,6 +78,12 @@ def setup_responses( f"https://advertising-api.amazon.com/{generic_response}", json=[], ) + if post_response: + responses.add( + responses.POST, + f"https://advertising-api.amazon.com/{post_response}", + json={}, + ) def get_all_stream_records(stream, stream_slice=None): @@ -254,20 +261,23 @@ def test_streams_displays( @pytest.mark.parametrize( ("stream_name", "endpoint"), [ - ("sponsored_brands_campaigns", "sb/campaigns"), - ("sponsored_brands_ad_groups", "sb/adGroups"), + ("sponsored_brands_campaigns", "sb/v4/campaigns/list"), + ("sponsored_brands_ad_groups", "sb/v4/adGroups/list"), ("sponsored_brands_keywords", "sb/keywords"), - ("sponsored_product_campaigns", "v2/sp/campaigns"), - ("sponsored_product_ad_groups", "v2/sp/adGroups"), - ("sponsored_product_keywords", "v2/sp/keywords"), - ("sponsored_product_negative_keywords", "v2/sp/negativeKeywords"), - ("sponsored_product_ads", "v2/sp/productAds"), - ("sponsored_product_targetings", "v2/sp/targets"), + ("sponsored_product_campaigns", "sp/campaigns/list"), + ("sponsored_product_ad_groups", "sp/adGroups/list"), + ("sponsored_product_keywords", "sp/keywords/list"), + ("sponsored_product_negative_keywords", "sp/negativeKeywords/list"), + ("sponsored_product_ads", "sp/productAds/list"), + ("sponsored_product_targetings", "sp/targets/list"), ], ) @responses.activate def test_streams_brands_and_products(config, stream_name, endpoint, profiles_response): - setup_responses(profiles_response=profiles_response, generic_response=endpoint) + if endpoint != "sb/keywords": + setup_responses(profiles_response=profiles_response, post_response=endpoint) + else: + setup_responses(profiles_response=profiles_response, generic_response=endpoint) source = SourceAmazonAds() streams = source.streams(config) @@ -282,8 +292,8 @@ def test_streams_brands_and_products(config, stream_name, endpoint, profiles_res def test_sponsored_product_ad_group_bid_recommendations_404_error(caplog, config, profiles_response): setup_responses(profiles_response=profiles_response) responses.add( - responses.GET, - "https://advertising-api.amazon.com/v2/sp/adGroups/xxx/bidRecommendations", + responses.POST, + "https://advertising-api.amazon.com/sp/targets/bid/recommendations", json={ "code": "404", "details": "404 Either the specified ad group identifier was not found or the specified ad group was found but no associated bid was found.", @@ -293,6 +303,6 @@ def test_sponsored_product_ad_group_bid_recommendations_404_error(caplog, config source = SourceAmazonAds() streams = source.streams(config) test_stream = get_stream_by_name(streams, "sponsored_product_ad_group_bid_recommendations") - records = get_all_stream_records(test_stream, stream_slice={"profileId": "1231", "adGroupId": "xxx"}) + records = get_all_stream_records(test_stream, stream_slice={"campaignId": "1231", "adGroupId": "xxx"}) assert records == [] assert "Skip current AdGroup because the specified ad group has no associated bid" in caplog.text diff --git a/docs/integrations/sources/amazon-ads-migrations.md b/docs/integrations/sources/amazon-ads-migrations.md index 11f3e15cb5ef..b9447fd491f9 100644 --- a/docs/integrations/sources/amazon-ads-migrations.md +++ b/docs/integrations/sources/amazon-ads-migrations.md @@ -1,5 +1,46 @@ # Amazon Ads Migration Guide +## Upgrading to 5.0.0 + +The following streams have updated schemas due to a change with the Amazon Ads API: + +* `SponsoredBrandsCampaigns` +* `SponsoredBrandsAdGroups` +* `SponsoredProductsCampaigns` +* `SponsoredProductsAdGroupBidRecommendations` + +### Schema Changes - Removed/Added Fields + +| Stream Name | Removed Fields | Added Fields | +|-------------------------------------------------|-----------------------------|--------------------------| +| `SponsoredBrandsCampaigns` | `serviceStatus`, `bidOptimization`, `bidMultiplier`, `adFormat`, `bidAdjustments`, `creative`, `landingPage`, `supplySource` | `ruleBasedBudget`, `bidding`, `productLocation`, `costType`, `smartDefault`, `extendedData` | +| `SponsoredBrandsAdGroups` | `bid`, `keywordId`, `keywordText`, `nativeLanuageKeyword`, `matchType` | `extendedData` | +| `SponsoredProductsCampaigns` | `campaignType`, `dailyBudget`, `ruleBasedBudget`, `premiumBidAdjustment`, `networks` | `dynamicBidding`, `budget`, `extendedData` | +| `SponsoredProductsAdGroupBidRecommendations` | `suggestedBid` | `theme`, `bidRecommendationsForTargetingExpressions` | + +### Refresh affected schemas and reset data + +1. Select **Connections** in the main navbar. + 1. Select the connection(s) affected by the update. +2. Select the **Replication** tab. + 1. Select **Refresh source schema**. + 2. Select **OK**. +```note +Any detected schema changes will be listed for your review. +``` +3. Select **Save changes** at the bottom of the page. + 1. Ensure the **Reset affected streams** option is checked. +```note +Depending on destination type you may not be prompted to reset your data. +``` +4. Select **Save connection**. +```note +This will reset the data in your destination and initiate a fresh sync. +``` + +For more information on resetting your data in Airbyte, see [this page](https://docs.airbyte.com/operator-guides/reset). + + ## Upgrading to 4.0.0 Streams `SponsoredBrandsAdGroups` and `SponsoredBrandsKeywords` now have updated schemas. @@ -19,7 +60,7 @@ Any detected schema changes will be listed for your review. ```note Depending on destination type you may not be prompted to reset your data. ``` -4. Select **Save connection**. +4. Select **Save connection**. ```note This will reset the data in your destination and initiate a fresh sync. ``` diff --git a/docs/integrations/sources/amazon-ads.md b/docs/integrations/sources/amazon-ads.md index 453ffbbe146f..196dc1fd4b1e 100644 --- a/docs/integrations/sources/amazon-ads.md +++ b/docs/integrations/sources/amazon-ads.md @@ -83,6 +83,10 @@ This source is capable of syncing the following streams: * [Products Reports](https://advertising.amazon.com/API/docs/en-us/sponsored-products/2-0/openapi#/Reports) * [Attribution Reports](https://advertising.amazon.com/API/docs/en-us/amazon-attribution-prod-3p/#/) +:::note +As of connector version 5.0.0, the `Sponsored Products Ad Group Bid Recommendations` stream provides bid recommendations and impact metrics for an existing automatic targeting ad group. The stream returns bid recommendations for match types `CLOSE_MATCH`, `LOOSE_MATCH`, `SUBSTITUTES`, and `COMPLEMENTS` per theme. For more detail on theme-based bid recommendations, review Amazon's [Theme-base bid suggestions - Quick-start guide](https://advertising.amazon.com/API/docs/en-us/guides/sponsored-products/bid-suggestions/theme-based-bid-suggestions-quickstart-guide). +::: + ## Connector-specific features and highlights All the reports are generated relative to the target profile's timezone. @@ -110,6 +114,7 @@ Information about expected report generation waiting time can be found [here](ht | Version | Date | Pull Request | Subject | |:--------|:-----------|:---------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------| +| 5.0.0 | 2024-03-22 | [36169](https://github.com/airbytehq/airbyte/pull/36169) | Update `SponsoredBrand` and `SponsoredProduct` streams due to API endpoint deprecation | | 4.1.0 | 2024-03-19 | [36267](https://github.com/airbytehq/airbyte/pull/36267) | Pin airbyte-cdk version to `^0` | | 4.0.4 | 2024-02-23 | [35481](https://github.com/airbytehq/airbyte/pull/35481) | Migrate source to `YamlDeclarativeSource` with custom `check_connection` | | 4.0.3 | 2024-02-12 | [35180](https://github.com/airbytehq/airbyte/pull/35180) | Manage dependencies with Poetry |