diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/.coveragerc b/airbyte-integrations/connectors/source-tiktok-marketing/.coveragerc new file mode 100644 index 000000000000..92d753c7c6ef --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/.coveragerc @@ -0,0 +1,3 @@ +[run] +omit = + source_tiktok_marketing/run.py diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/abnormal_state.json b/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/abnormal_state.json index 38b317323132..3b7690cfe7aa 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/abnormal_state.json +++ b/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/abnormal_state.json @@ -6,7 +6,17 @@ "name": "ad_group_audience_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -17,7 +27,17 @@ "name": "ads_audience_reports_by_province_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -28,7 +48,17 @@ "name": "ad_groups_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -39,7 +69,26 @@ "name": "ad_groups" }, "stream_state": { - "modify_time": "2099-01-01 01:00:00" + "states": [ + { + "partition": { + "advertiser_id": "7001035076276387841", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01 01:01:01" + } + }, + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01 01:01:01" + } + } + ] } } }, @@ -50,7 +99,17 @@ "name": "ads_audience_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -61,7 +120,17 @@ "name": "ads_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -72,7 +141,26 @@ "name": "ads" }, "stream_state": { - "modify_time": "2099-01-01 01:00:0" + "states": [ + { + "partition": { + "advertiser_id": "7001035076276387841", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01 01:01:01" + } + }, + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01 01:01:01" + } + } + ] } } }, @@ -83,7 +171,17 @@ "name": "advertisers_audience_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -94,7 +192,17 @@ "name": "advertisers_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -105,7 +213,17 @@ "name": "campaigns_audience_reports_by_country_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -116,7 +234,17 @@ "name": "campaigns_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -127,7 +255,26 @@ "name": "campaigns" }, "stream_state": { - "modify_time": "2099-01-01 01:00:0" + "states": [ + { + "partition": { + "advertiser_id": "7001035076276387841", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01 01:01:01" + } + }, + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01 01:01:01" + } + } + ] } } }, @@ -138,7 +285,26 @@ "name": "creative_assets_images" }, "stream_state": { - "modify_time": "2099-01-01 01:00:0" + "states": [ + { + "partition": { + "advertiser_id": "7001035076276387841", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01T01:01:01Z" + } + }, + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01T01:01:01Z" + } + } + ] } } }, @@ -149,7 +315,26 @@ "name": "creative_assets_videos" }, "stream_state": { - "modify_time": "2099-01-01 01:00:0" + "states": [ + { + "partition": { + "advertiser_id": "7001035076276387841", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01T01:01:01Z" + } + }, + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "modify_time": "2222-01-01T01:01:01Z" + } + } + ] } } }, @@ -160,7 +345,17 @@ "name": "ad_group_audience_reports_by_platform_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -171,7 +366,17 @@ "name": "campaigns_audience_reports_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -182,7 +387,17 @@ "name": "campaigns_audience_reports_by_platform_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -193,7 +408,17 @@ "name": "ad_group_audience_reports_by_country_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -204,7 +429,17 @@ "name": "advertisers_audience_reports_by_platform_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -215,7 +450,17 @@ "name": "ads_audience_reports_by_country_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -226,7 +471,17 @@ "name": "ads_audience_reports_by_platform_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } }, @@ -237,7 +492,17 @@ "name": "advertisers_audience_reports_by_country_daily" }, "stream_state": { - "stat_time_day": "2099-01-01" + "states": [ + { + "partition": { + "advertiser_id": "7002238017842757633", + "parent_slice": {} + }, + "cursor": { + "stat_time_day": "2222-01-01 00:00:00" + } + } + ] } } } diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records.jsonl b/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records.jsonl index 0efab7420dd8..3721d0552c72 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records.jsonl +++ b/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records.jsonl @@ -16,9 +16,9 @@ {"stream": "creative_assets_videos", "data": {"size": 13605359, "signature": "ded8dcda6363d0219764ba5246a673ad", "file_name": "Video16974675945951_Whale, sea, electronica(859574)", "width": 720, "create_time": "2023-10-16T14:46:50Z", "bit_rate": 3626524, "allow_download": true, "modify_time": "2023-10-16T14:46:50Z", "height": 1280, "material_id": "7290567851409981441", "preview_url_expire_time": "2023-10-23 17:48:23", "video_cover_url": "http://p16-sign-sg.tiktokcdn.com/tos-alisg-p-0051c001-sg/oEaaNUsAlIQwDnBGmBLecBzDQGnACMA0Xgbfb5~tplv-noop.image?x-expires=1698083303&x-signature=%2Fq08ZTEll8KgfYXlg%2B1r8oqCgBk%3D", "allowed_placements": ["PLACEMENT_TOPBUZZ", "PLACEMENT_TIKTOK", "PLACEMENT_HELO", "PLACEMENT_PANGLE", "PLACEMENT_GLOBAL_APP_BUNDLE"], "video_id": "v10033g50000ckmkplrc77u30n4dkdb0", "preview_url": "http://v16m-default.akamaized.net/4a8d0298528bfaa92661ed8b31e822b7/6536b1e7/video/tos/alisg/tos-alisg-ve-0051c001-sg/oQABgVatYClIbB2FGEnU0QXmQLTNDzegfWGDs5/?a=0&ch=0&cr=0&dr=0&lr=ad&cd=0%7C0%7C0%7C0&cv=1&br=1630&bt=815&bti=Njs0Zi8tOg%3D%3D&cs=0&ds=3&ft=dl9~j-Inz7T8KqFZiyq8Z&mime_type=video_mp4&qs=0&rc=ZTw5M2c4ZDhoODllM2VmNkBpM2dncTY6Zm9ubjMzODYzNEAxXy40YDUyXzAxYmMwMGItYSNfaDEtcjRvbWpgLS1kMC1zcw%3D%3D&l=202310231147525B364A25545CD403DD31&btag=e00088000", "duration": 30.013, "displayable": true, "format": "mp4"}, "emitted_at": 1698061673747} {"stream": "creative_assets_videos", "data": {"size": 2150312, "signature": "61c2035644dbcea75ef315af73a120ea", "file_name": "Optimized Version 3_202203281449", "width": 720, "create_time": "2022-03-28T11:49:10Z", "bit_rate": 3430892, "allow_download": true, "modify_time": "2022-03-28T11:49:12Z", "height": 1280, "material_id": "7080116206691549186", "preview_url_expire_time": "2023-10-23 17:47:58", "video_cover_url": "http://p16-sign-sg.tiktokcdn.com/v0201/7f371ff6f0764f8b8ef4f37d7b980d50~tplv-noop.image?x-expires=1698083278&x-signature=1Gq16fQpsQPQsQwk5nzMZoQ5GcY%3D", "allowed_placements": ["PLACEMENT_TOPBUZZ", "PLACEMENT_TIKTOK", "PLACEMENT_HELO", "PLACEMENT_PANGLE", "PLACEMENT_GLOBAL_APP_BUNDLE"], "video_id": "v10033g50000c90q1d3c77ub6e96fvo0", "preview_url": "http://v16m-default.akamaized.net/47236ddfa95a07a9b22fc7b37b96568b/6536b1ce/video/tos/alisg/tos-alisg-v-0000/8968c64a5dc6489a89fe324a6156634d/?a=0&ch=0&cr=0&dr=0&lr=ad&cd=0%7C0%7C0%7C0&cv=1&br=1664&bt=832&bti=Njs0Zi8tOg%3D%3D&cs=0&ds=3&ft=dl9~j-Inz7T8KqFZiyq8Z&mime_type=video_mp4&qs=0&rc=NjY6aDpoNmY0aTRnNmczOEBpM3k5aGU6Zmd0PDMzODYzNEAvYzZiYTAuNTYxMzUxYDZjYSNsYzYzcjQwLi1gLS1kMC1zcw%3D%3D&l=202310231147525B364A25545CD403DD31&btag=e00088000", "duration": 5.014, "displayable": true, "format": "mp4"}, "emitted_at": 1698061673748} {"stream": "creative_assets_videos", "data": {"size": 2338817, "signature": "f95bfbd2c3d6f6c16d3bd048447b3b73", "file_name": "7021053237617754114", "width": 720, "create_time": "2021-10-20T08:04:10Z", "bit_rate": 1867878, "allow_download": false, "modify_time": "2022-03-28T12:08:49Z", "height": 1280, "material_id": "7021053237617754114", "preview_url_expire_time": "2023-10-23 17:48:03", "video_cover_url": "http://p16-sign-sg.tiktokcdn.com/v0201/8f77082a1f3c40c586f8282356490c58~tplv-noop.image?x-expires=1698083283&x-signature=LCEIxKOuPrFQFqfrpoGE2K6tK00%3D", "allowed_placements": ["PLACEMENT_TOPBUZZ", "PLACEMENT_TIKTOK", "PLACEMENT_HELO", "PLACEMENT_PANGLE", "PLACEMENT_GLOBAL_APP_BUNDLE"], "video_id": "v10033g50000c5nsqcbc77ubdn136b70", "preview_url": "http://v16m-default.akamaized.net/cb4fc19071c911726067bfea11fe06ec/6536b1d3/video/tos/alisg/tos-alisg-v-0000/43f1d52d089a4e428e31dcf356d66906/?a=0&ch=0&cr=0&dr=0&lr=ad&cd=0%7C0%7C0%7C0&cv=1&br=1642&bt=821&bti=Njs0Zi8tOg%3D%3D&cs=0&ds=3&ft=dl9~j-Inz7T8KqFZiyq8Z&mime_type=video_mp4&qs=0&rc=OzpoOWc2OzdpN2hnOjtoOEBpM2U2cWU6ZmZ2ODMzODYzNEAzLTMtNGAuNl8xMmE2MGA1YSM0My5hcjRfbmtgLS1kMC1zcw%3D%3D&l=202310231147525B364A25545CD403DD31&btag=e00088000", "duration": 10.017, "displayable": true, "format": "mp4"}, "emitted_at": 1698061673749} -{"stream": "advertiser_ids", "data": {"advertiser_id": 7001035076276387841, "advertiser_name": "Airbyte0827"}, "emitted_at": 1698061674290} -{"stream": "advertiser_ids", "data": {"advertiser_id": 7001040009704833026, "advertiser_name": "Airbyte08270"}, "emitted_at": 1698061674291} -{"stream": "advertiser_ids", "data": {"advertiser_id": 7002238017842757633, "advertiser_name": "Airbyte0830"}, "emitted_at": 1698061674292} +{"stream": "advertiser_ids", "data": {"advertiser_id": "7001035076276387841", "advertiser_name": "Airbyte0827"}, "emitted_at": 1698061674290} +{"stream": "advertiser_ids", "data": {"advertiser_id": "7001040009704833026", "advertiser_name": "Airbyte08270"}, "emitted_at": 1698061674291} +{"stream": "advertiser_ids", "data": {"advertiser_id": "7002238017842757633", "advertiser_name": "Airbyte0830"}, "emitted_at": 1698061674292} {"stream": "ads_reports_daily", "data": {"dimensions": {"ad_id": 1714125051115569, "stat_time_day": "2021-10-25 00:00:00"}, "metrics": {"cpm": "3.43", "shares": 0, "real_time_cost_per_result": "0.290", "video_views_p75": 140, "follows": 0, "comments": 0, "mobile_app_id": "0", "tt_app_id": 0, "video_watched_6s": 180, "cost_per_result": "0.290", "average_video_play_per_user": 1.64, "cta_purchase": "0", "real_time_conversion": "0", "real_time_cost_per_conversion": "0.00", "promotion_type": "Website", "video_views_p50": 214, "cost_per_secondary_goal_result": null, "ctr": "1.18", "real_time_result_rate": "1.18", "real_time_app_install_cost": 0, "impressions": "5830", "conversion": "0", "cta_conversion": "0", "placement_type": "Automatic Placement", "profile_visits": 0, "result": "69", "cost_per_1000_reached": "4.16", "video_views_p25": 513, "campaign_id": 1714125042508817, "vta_purchase": "0", "tt_app_name": "0", "onsite_shopping": "0", "total_pageview": "0", "cpc": "0.29", "complete_payment": "0", "dpa_target_audience_type": null, "total_onsite_shopping_value": "0.00", "vta_conversion": "0", "spend": "20.00", "real_time_result": "69", "secondary_goal_result_rate": null, "conversion_rate": "0.00", "secondary_goal_result": null, "adgroup_name": "Ad Group20211020010107", "total_purchase_value": "0.00", "result_rate": "1.18", "ad_text": "Airbyte - data portabioolity platform - from anywhere to anywhere!", "ad_name": "Optimized Version 4_202110201102_2021-10-20 11:02:00", "likes": 36, "video_watched_2s": 686, "real_time_app_install": 0, "reach": "4806", "total_complete_payment_rate": "0.00", "clicks": "69", "cost_per_conversion": "0.00", "app_install": 0, "real_time_conversion_rate": "0.00", "video_play_actions": 5173, "value_per_complete_payment": "0.00", "frequency": "1.21", "average_video_play": 1.52, "video_views_p100": 92, "clicks_on_music_disc": 0, "adgroup_id": 1714125049901106, "campaign_name": "Website Traffic20211020010104"}, "stat_time_day": "2021-10-25 00:00:00", "ad_id": 1714125051115569}, "emitted_at": 1698062442958} {"stream": "ads_reports_daily", "data": {"dimensions": {"ad_id": 1714125051115569, "stat_time_day": "2021-10-20 00:00:00"}, "metrics": {"cpm": "5.31", "shares": 0, "real_time_cost_per_result": "0.377", "video_views_p75": 74, "follows": 0, "comments": 1, "mobile_app_id": "0", "tt_app_id": 0, "video_watched_6s": 106, "cost_per_result": "0.377", "average_video_play_per_user": 1.55, "cta_purchase": "0", "real_time_conversion": "0", "real_time_cost_per_conversion": "0.00", "promotion_type": "Website", "video_views_p50": 130, "cost_per_secondary_goal_result": null, "ctr": "1.41", "real_time_result_rate": "1.41", "real_time_app_install_cost": 0, "impressions": "3765", "conversion": "0", "cta_conversion": "0", "placement_type": "Automatic Placement", "profile_visits": 0, "result": "53", "cost_per_1000_reached": "6.38", "video_views_p25": 295, "campaign_id": 1714125042508817, "vta_purchase": "0", "tt_app_name": "0", "onsite_shopping": "0", "total_pageview": "0", "cpc": "0.38", "complete_payment": "0", "dpa_target_audience_type": null, "total_onsite_shopping_value": "0.00", "vta_conversion": "0", "spend": "20.00", "real_time_result": "53", "secondary_goal_result_rate": null, "conversion_rate": "0.00", "secondary_goal_result": null, "adgroup_name": "Ad Group20211020010107", "total_purchase_value": "0.00", "result_rate": "1.41", "ad_text": "Airbyte - data portabioolity platform - from anywhere to anywhere!", "ad_name": "Optimized Version 4_202110201102_2021-10-20 11:02:00", "likes": 36, "video_watched_2s": 408, "real_time_app_install": 0, "reach": "3134", "total_complete_payment_rate": "0.00", "clicks": "53", "cost_per_conversion": "0.00", "app_install": 0, "real_time_conversion_rate": "0.00", "video_play_actions": 3344, "value_per_complete_payment": "0.00", "frequency": "1.20", "average_video_play": 1.45, "video_views_p100": 52, "clicks_on_music_disc": 0, "adgroup_id": 1714125049901106, "campaign_name": "Website Traffic20211020010104"}, "stat_time_day": "2021-10-20 00:00:00", "ad_id": 1714125051115569}, "emitted_at": 1698062442963} {"stream": "ads_reports_daily", "data": {"dimensions": {"ad_id": 1714125051115569, "stat_time_day": "2021-10-26 00:00:00"}, "metrics": {"cpm": "5.33", "shares": 0, "real_time_cost_per_result": "0.435", "video_views_p75": 90, "follows": 0, "comments": 1, "mobile_app_id": "0", "tt_app_id": 0, "video_watched_6s": 112, "cost_per_result": "0.435", "average_video_play_per_user": 1.61, "cta_purchase": "0", "real_time_conversion": "0", "real_time_cost_per_conversion": "0.00", "promotion_type": "Website", "video_views_p50": 142, "cost_per_secondary_goal_result": null, "ctr": "1.23", "real_time_result_rate": "1.23", "real_time_app_install_cost": 0, "impressions": "3750", "conversion": "0", "cta_conversion": "0", "placement_type": "Automatic Placement", "profile_visits": 0, "result": "46", "cost_per_1000_reached": "6.41", "video_views_p25": 297, "campaign_id": 1714125042508817, "vta_purchase": "0", "tt_app_name": "0", "onsite_shopping": "0", "total_pageview": "0", "cpc": "0.43", "complete_payment": "0", "dpa_target_audience_type": null, "total_onsite_shopping_value": "0.00", "vta_conversion": "0", "spend": "20.00", "real_time_result": "46", "secondary_goal_result_rate": null, "conversion_rate": "0.00", "secondary_goal_result": null, "adgroup_name": "Ad Group20211020010107", "total_purchase_value": "0.00", "result_rate": "1.23", "ad_text": "Airbyte - data portabioolity platform - from anywhere to anywhere!", "ad_name": "Optimized Version 4_202110201102_2021-10-20 11:02:00", "likes": 25, "video_watched_2s": 413, "real_time_app_install": 0, "reach": "3119", "total_complete_payment_rate": "0.00", "clicks": "46", "cost_per_conversion": "0.00", "app_install": 0, "real_time_conversion_rate": "0.00", "video_play_actions": 3344, "value_per_complete_payment": "0.00", "frequency": "1.20", "average_video_play": 1.5, "video_views_p100": 71, "clicks_on_music_disc": 0, "adgroup_id": 1714125049901106, "campaign_name": "Website Traffic20211020010104"}, "stat_time_day": "2021-10-26 00:00:00", "ad_id": 1714125051115569}, "emitted_at": 1698062442968} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records2.jsonl b/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records2.jsonl index c01f830dbef7..820755928b66 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records2.jsonl +++ b/airbyte-integrations/connectors/source-tiktok-marketing/integration_tests/expected_records2.jsonl @@ -30,3 +30,29 @@ {"stream": "advertisers_audience_reports", "data": {"metrics": {"cpm": "6.75", "ctr": "1.79", "impressions": "335", "spend": "2.26", "clicks": "6", "cpc": "0.38"}, "dimensions": {"age": "AGE_18_24", "gender": "MALE", "stat_time_day": "2021-10-19 00:00:00", "advertiser_id": 7002238017842757633}, "stat_time_day": "2021-10-19 00:00:00", "advertiser_id": 7002238017842757633, "gender": "MALE", "age": "AGE_18_24"}, "emitted_at": 1698066026190} {"stream": "advertisers_audience_reports", "data": {"metrics": {"cpm": "0.00", "ctr": "2.86", "impressions": "35", "spend": "0.00", "clicks": "1", "cpc": "0.00"}, "dimensions": {"age": "AGE_35_44", "gender": "MALE", "stat_time_day": "2021-10-19 00:00:00", "advertiser_id": 7002238017842757633}, "stat_time_day": "2021-10-19 00:00:00", "advertiser_id": 7002238017842757633, "gender": "MALE", "age": "AGE_35_44"}, "emitted_at": 1698066026193} {"stream": "advertisers_audience_reports", "data": {"metrics": {"cpm": "3.88", "ctr": "1.35", "impressions": "2146", "spend": "8.32", "clicks": "29", "cpc": "0.29"}, "dimensions": {"age": "AGE_13_17", "gender": "FEMALE", "stat_time_day": "2021-10-19 00:00:00", "advertiser_id": 7002238017842757633}, "stat_time_day": "2021-10-19 00:00:00", "advertiser_id": 7002238017842757633, "gender": "FEMALE", "age": "AGE_13_17"}, "emitted_at": 1698066026196} +{"stream": "campaigns_audience_reports", "data": {"dimensions": {"age": "AGE_18_24", "gender": "MALE", "campaign_id": 1779923887578145, "stat_time_day": "2023-10-16 00:00:00"}, "metrics": {"spend": "0.00", "clicks": "0", "campaign_name": "UTM_PARAMSTraffic20231016173112", "ctr": "0.00", "cpm": "0.00", "cpc": "0.00", "impressions": "31"}, "stat_time_day": "2023-10-16 00:00:00", "campaign_id": 1779923887578145, "gender": "MALE", "age": "AGE_18_24"}, "emitted_at": 1716548552147} +{"stream": "campaigns_audience_reports", "data": {"dimensions": {"age": "AGE_55_100", "gender": "FEMALE", "campaign_id": 1779923887578145, "stat_time_day": "2023-10-16 00:00:00"}, "metrics": {"spend": "0.00", "clicks": "0", "campaign_name": "UTM_PARAMSTraffic20231016173112", "ctr": "0.00", "cpm": "0.00", "cpc": "0.00", "impressions": "5"}, "stat_time_day": "2023-10-16 00:00:00", "campaign_id": 1779923887578145, "gender": "FEMALE", "age": "AGE_55_100"}, "emitted_at": 1716548552153} +{"stream": "campaigns_audience_reports", "data": {"dimensions": {"age": "AGE_45_54", "gender": "MALE", "campaign_id": 1779923887578145, "stat_time_day": "2023-10-16 00:00:00"}, "metrics": {"spend": "0.00", "clicks": "0", "campaign_name": "UTM_PARAMSTraffic20231016173112", "ctr": "0.00", "cpm": "0.00", "cpc": "0.00", "impressions": "1"}, "stat_time_day": "2023-10-16 00:00:00", "campaign_id": 1779923887578145, "gender": "MALE", "age": "AGE_45_54"}, "emitted_at": 1716548552159} +{"stream": "ad_group_audience_reports_by_platform", "data": {"metrics": {"campaign_id": 1779923887578145, "real_time_result": "61", "spend": "9.09", "adgroup_name": "Ad group 20231016073545", "real_time_result_rate": "2.40", "tt_app_name": "0", "cpc": "0.15", "dpa_target_audience_type": "-", "result": "61", "result_rate": "2.40", "conversion_rate": "0.00", "real_time_cost_per_conversion": "0.00", "cost_per_result": "0.15", "tt_app_id": "0", "impressions": "2546", "placement_type": "Automatic Placement", "promotion_type": "Website", "campaign_name": "UTM_PARAMSTraffic20231016173112", "real_time_cost_per_result": "0.15", "mobile_app_id": "0", "clicks": "61", "real_time_conversion": "0", "conversion": "0", "cost_per_conversion": "0.00", "cpm": "3.57", "ctr": "2.40", "real_time_conversion_rate": "0.00"}, "dimensions": {"adgroup_id": 1779923881029666, "platform": "IPHONE", "stat_time_day": "2023-10-16 00:00:00"}, "stat_time_day": "2023-10-16 00:00:00", "adgroup_id": 1779923881029666, "platform": "IPHONE"}, "emitted_at": 1716548748341} +{"stream": "ad_group_audience_reports_by_platform", "data": {"metrics": {"campaign_id": 1779923887578145, "real_time_result": "0", "spend": "0.00", "adgroup_name": "Ad group 20231016073545", "real_time_result_rate": "0.00", "tt_app_name": "0", "cpc": "0.00", "dpa_target_audience_type": "-", "result": "0", "result_rate": "0.00", "conversion_rate": "0.00", "real_time_cost_per_conversion": "0.00", "cost_per_result": "0.00", "tt_app_id": "0", "impressions": "7", "placement_type": "Automatic Placement", "promotion_type": "Website", "campaign_name": "UTM_PARAMSTraffic20231016173112", "real_time_cost_per_result": "0.00", "mobile_app_id": "0", "clicks": "0", "real_time_conversion": "0", "conversion": "0", "cost_per_conversion": "0.00", "cpm": "0.00", "ctr": "0.00", "real_time_conversion_rate": "0.00"}, "dimensions": {"adgroup_id": 1779923881029666, "platform": "ANDROID", "stat_time_day": "2023-10-16 00:00:00"}, "stat_time_day": "2023-10-16 00:00:00", "adgroup_id": 1779923881029666, "platform": "ANDROID"}, "emitted_at": 1716548748346} +{"stream": "ad_group_audience_reports_by_platform", "data": {"metrics": {"campaign_id": 1779923887578145, "real_time_result": "5", "spend": "0.91", "adgroup_name": "Ad group 20231016073545", "real_time_result_rate": "2.91", "tt_app_name": "0", "cpc": "0.18", "dpa_target_audience_type": "-", "result": "5", "result_rate": "2.91", "conversion_rate": "0.00", "real_time_cost_per_conversion": "0.00", "cost_per_result": "0.18", "tt_app_id": "0", "impressions": "172", "placement_type": "Automatic Placement", "promotion_type": "Website", "campaign_name": "UTM_PARAMSTraffic20231016173112", "real_time_cost_per_result": "0.18", "mobile_app_id": "0", "clicks": "5", "real_time_conversion": "0", "conversion": "0", "cost_per_conversion": "0.00", "cpm": "5.29", "ctr": "2.91", "real_time_conversion_rate": "0.00"}, "dimensions": {"adgroup_id": 1779923881029666, "platform": "IPAD", "stat_time_day": "2023-10-16 00:00:00"}, "stat_time_day": "2023-10-16 00:00:00", "adgroup_id": 1779923881029666, "platform": "IPAD"}, "emitted_at": 1716548748351} +{"stream": "ad_group_audience_reports_by_country", "data": {"dimensions": {"stat_time_day": "2022-03-29 00:00:00", "country_code": "US", "adgroup_id": 1728545385226289}, "metrics": {"cpc": "0.35", "campaign_name": "CampaignVadimTraffic", "promotion_type": "Website", "campaign_id": 1728545382536225, "real_time_result": "57", "mobile_app_id": "0", "real_time_cost_per_conversion": "0.00", "real_time_conversion_rate": "0.00", "cost_per_result": "0.35", "spend": "20.00", "real_time_cost_per_result": "0.35", "ctr": "0.93", "conversion_rate": "0.00", "conversion": "0", "tt_app_name": "0", "dpa_target_audience_type": "-", "real_time_result_rate": "0.93", "clicks": "57", "cpm": "3.26", "placement_type": "Automatic Placement", "cost_per_conversion": "0.00", "result_rate": "0.93", "tt_app_id": "0", "impressions": "6137", "adgroup_name": "AdGroupVadim", "real_time_conversion": "0", "result": "57"}, "stat_time_day": "2022-03-29 00:00:00", "adgroup_id": 1728545385226289, "country_code": "US"}, "emitted_at": 1716549438311} +{"stream": "ad_group_audience_reports_by_country", "data": {"metrics": {"promotion_type": "Website", "tt_app_id": "0", "real_time_conversion_rate": "0.00", "cost_per_conversion": "0.00", "mobile_app_id": "0", "placement_type": "Automatic Placement", "conversion_rate": "0.00", "campaign_id": 1714125042508817, "result_rate": "0.00", "cpc": "0.00", "clicks": "0", "real_time_cost_per_result": "0.00", "dpa_target_audience_type": "-", "spend": "0.00", "result": "0", "real_time_result_rate": "0.00", "real_time_result": "0", "impressions": "0", "cost_per_result": "0.00", "campaign_name": "Website Traffic20211020010104", "tt_app_name": "0", "conversion": "0", "adgroup_name": "Ad Group20211020010107", "cpm": "0.00", "real_time_cost_per_conversion": "0.00", "real_time_conversion": "0", "ctr": "0.00"}, "dimensions": {"adgroup_id": 1714125049901106, "stat_time_day": "2022-05-18 00:00:00", "country_code": "US"}, "stat_time_day": "2022-05-18 00:00:00", "adgroup_id": 1714125049901106, "country_code": "US"}, "emitted_at": 1716549439431} +{"stream": "ad_group_audience_reports_by_country", "data": {"metrics": {"real_time_conversion_rate": "0.00", "cpm": "3.67", "real_time_cost_per_conversion": "0.00", "cpc": "0.15", "placement_type": "Automatic Placement", "campaign_id": 1779923887578145, "mobile_app_id": "0", "tt_app_id": "0", "real_time_result_rate": "2.42", "dpa_target_audience_type": "-", "impressions": "2725", "cost_per_result": "0.15", "ctr": "2.42", "real_time_result": "66", "cost_per_conversion": "0.00", "campaign_name": "UTM_PARAMSTraffic20231016173112", "tt_app_name": "0", "adgroup_name": "Ad group 20231016073545", "result_rate": "2.42", "spend": "10.00", "result": "66", "promotion_type": "Website", "conversion_rate": "0.00", "clicks": "66", "conversion": "0", "real_time_cost_per_result": "0.15", "real_time_conversion": "0"}, "dimensions": {"country_code": "US", "stat_time_day": "2023-10-16 00:00:00", "adgroup_id": 1779923881029666}, "stat_time_day": "2023-10-16 00:00:00", "adgroup_id": 1779923881029666, "country_code": "US"}, "emitted_at": 1716549458277} +{"stream": "ads_audience_reports_by_country", "data": {"dimensions": {"ad_id": 1779923894506609, "country_code": "US", "stat_time_day": "2023-10-16 00:00:00"}, "metrics": {"cost_per_result": "0.15", "result_rate": "2.70", "impressions": "2335", "conversion_rate": "0.00", "cpm": "3.97", "real_time_conversion_rate": "0.00", "tt_app_id": "0", "mobile_app_id": "0", "clicks": "63", "real_time_cost_per_result": "0.15", "ctr": "2.70", "conversion": "0", "real_time_result_rate": "2.70", "adgroup_name": "Ad group 20231016073545", "tt_app_name": "0", "adgroup_id": 1779923881029666, "ad_text": "airbyte", "placement_type": "Automatic Placement", "spend": "9.26", "cost_per_conversion": "0.00", "real_time_result": "63", "cpc": "0.15", "campaign_name": "UTM_PARAMSTraffic20231016173112", "result": "63", "campaign_id": 1779923887578145, "ad_name": "Video16974675945951_Whale, sea, electronica(859574)_2023-10-16 07:46:35", "dpa_target_audience_type": "-", "real_time_conversion": "0", "promotion_type": "Website", "real_time_cost_per_conversion": "0.00"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "country_code": "US"}, "emitted_at": 1716549916816} +{"stream": "ads_audience_reports_by_country", "data": {"dimensions": {"ad_id": 1779923894511665, "country_code": "US", "stat_time_day": "2023-10-16 00:00:00"}, "metrics": {"cost_per_result": "0.25", "result_rate": "0.77", "impressions": "390", "conversion_rate": "0.00", "cpm": "1.90", "real_time_conversion_rate": "0.00", "tt_app_id": "0", "mobile_app_id": "0", "clicks": "3", "real_time_cost_per_result": "0.25", "ctr": "0.77", "conversion": "0", "real_time_result_rate": "0.77", "adgroup_name": "Ad group 20231016073545", "tt_app_name": "0", "adgroup_id": 1779923881029666, "ad_text": "airbyte", "placement_type": "Automatic Placement", "spend": "0.74", "cost_per_conversion": "0.00", "real_time_result": "3", "cpc": "0.25", "campaign_name": "UTM_PARAMSTraffic20231016173112", "result": "3", "campaign_id": 1779923887578145, "ad_name": "Video16974675946002_Heartwarming Atmosphere Pops with Piano Main(827850)_2023-10-16 07:46:35", "dpa_target_audience_type": "-", "real_time_conversion": "0", "promotion_type": "Website", "real_time_cost_per_conversion": "0.00"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894511665, "country_code": "US"}, "emitted_at": 1716549916821} +{"stream": "advertisers_audience_reports_by_country", "data": {"metrics": {"cpc": "0.00", "cpm": "0.00", "spend": "0.00", "ctr": "0.00", "impressions": "0", "clicks": "0"}, "dimensions": {"stat_time_day": "2022-04-02 00:00:00", "advertiser_id": 7002238017842757633, "country_code": "US"}, "stat_time_day": "2022-04-02 00:00:00", "advertiser_id": 7002238017842757633, "country_code": "US"}, "emitted_at": 1716550397762} +{"stream": "advertisers_audience_reports_by_country", "data": {"metrics": {"cpm": "0.00", "clicks": "0", "cpc": "0.00", "impressions": "0", "ctr": "0.00", "spend": "0.00"}, "dimensions": {"stat_time_day": "2022-05-18 00:00:00", "advertiser_id": 7002238017842757633, "country_code": "US"}, "stat_time_day": "2022-05-18 00:00:00", "advertiser_id": 7002238017842757633, "country_code": "US"}, "emitted_at": 1716550398276} +{"stream": "advertisers_audience_reports_by_country", "data": {"dimensions": {"country_code": "US", "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633}, "metrics": {"ctr": "2.42", "clicks": "66", "spend": "10.00", "impressions": "2725", "cpm": "3.67", "cpc": "0.15"}, "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633, "country_code": "US"}, "emitted_at": 1716550407969} +{"stream": "campaigns_audience_reports_by_platform", "data": {"metrics": {"clicks": "5", "ctr": "2.91", "cpc": "0.18", "spend": "0.91", "impressions": "172", "campaign_name": "UTM_PARAMSTraffic20231016173112", "cpm": "5.29"}, "dimensions": {"campaign_id": 1779923887578145, "platform": "IPAD", "stat_time_day": "2023-10-16 00:00:00"}, "stat_time_day": "2023-10-16 00:00:00", "campaign_id": 1779923887578145, "platform": "IPAD"}, "emitted_at": 1716550603828} +{"stream": "campaigns_audience_reports_by_platform", "data": {"metrics": {"clicks": "0", "ctr": "0.00", "cpc": "0.00", "spend": "0.00", "impressions": "7", "campaign_name": "UTM_PARAMSTraffic20231016173112", "cpm": "0.00"}, "dimensions": {"campaign_id": 1779923887578145, "platform": "ANDROID", "stat_time_day": "2023-10-16 00:00:00"}, "stat_time_day": "2023-10-16 00:00:00", "campaign_id": 1779923887578145, "platform": "ANDROID"}, "emitted_at": 1716550603833} +{"stream": "campaigns_audience_reports_by_platform", "data": {"metrics": {"clicks": "61", "ctr": "2.40", "cpc": "0.15", "spend": "9.09", "impressions": "2546", "campaign_name": "UTM_PARAMSTraffic20231016173112", "cpm": "3.57"}, "dimensions": {"campaign_id": 1779923887578145, "platform": "IPHONE", "stat_time_day": "2023-10-16 00:00:00"}, "stat_time_day": "2023-10-16 00:00:00", "campaign_id": 1779923887578145, "platform": "IPHONE"}, "emitted_at": 1716550603838} +{"stream": "advertisers_audience_reports_by_platform", "data": {"metrics": {"ctr": "2.91", "cpc": "0.18", "spend": "0.91", "impressions": "172", "clicks": "5", "cpm": "5.29"}, "dimensions": {"platform": "IPAD", "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633}, "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633, "platform": "IPAD"}, "emitted_at": 1716550796752} +{"stream": "advertisers_audience_reports_by_platform", "data": {"metrics": {"ctr": "0.00", "cpc": "0.00", "spend": "0.00", "impressions": "7", "clicks": "0", "cpm": "0.00"}, "dimensions": {"platform": "ANDROID", "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633}, "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633, "platform": "ANDROID"}, "emitted_at": 1716550796757} +{"stream": "advertisers_audience_reports_by_platform", "data": {"metrics": {"ctr": "2.40", "cpc": "0.15", "spend": "9.09", "impressions": "2546", "clicks": "61", "cpm": "3.57"}, "dimensions": {"platform": "IPHONE", "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633}, "stat_time_day": "2023-10-16 00:00:00", "advertiser_id": 7002238017842757633, "platform": "IPHONE"}, "emitted_at": 1716550796762} +{"stream": "ads_audience_reports_by_platform", "data": {"metrics": {"real_time_conversion_rate": "0.00", "cpm": "0.00", "real_time_cost_per_conversion": "0.00", "cpc": "0.00", "placement_type": "Automatic Placement", "campaign_id": 1779923887578145, "mobile_app_id": "0", "tt_app_id": "0", "real_time_result_rate": "0.00", "dpa_target_audience_type": "-", "impressions": "3", "cost_per_result": "0.00", "ctr": "0.00", "real_time_result": "0", "cost_per_conversion": "0.00", "campaign_name": "UTM_PARAMSTraffic20231016173112", "tt_app_name": "0", "adgroup_name": "Ad group 20231016073545", "result_rate": "0.00", "spend": "0.00", "result": "0", "ad_name": "Video16974675945951_Whale, sea, electronica(859574)_2023-10-16 07:46:35", "promotion_type": "Website", "ad_text": "airbyte", "conversion_rate": "0.00", "clicks": "0", "conversion": "0", "adgroup_id": 1779923881029666, "real_time_cost_per_result": "0.00", "real_time_conversion": "0"}, "dimensions": {"stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "platform": "ANDROID"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "platform": "ANDROID"}, "emitted_at": 1716550966449} +{"stream": "ads_audience_reports_by_platform", "data": {"metrics": {"real_time_conversion_rate": "0.00", "cpm": "3.95", "real_time_cost_per_conversion": "0.00", "cpc": "0.15", "placement_type": "Automatic Placement", "campaign_id": 1779923887578145, "mobile_app_id": "0", "tt_app_id": "0", "real_time_result_rate": "2.69", "dpa_target_audience_type": "-", "impressions": "2197", "cost_per_result": "0.15", "ctr": "2.69", "real_time_result": "59", "cost_per_conversion": "0.00", "campaign_name": "UTM_PARAMSTraffic20231016173112", "tt_app_name": "0", "adgroup_name": "Ad group 20231016073545", "result_rate": "2.69", "spend": "8.67", "result": "59", "ad_name": "Video16974675945951_Whale, sea, electronica(859574)_2023-10-16 07:46:35", "promotion_type": "Website", "ad_text": "airbyte", "conversion_rate": "0.00", "clicks": "59", "conversion": "0", "adgroup_id": 1779923881029666, "real_time_cost_per_result": "0.15", "real_time_conversion": "0"}, "dimensions": {"stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "platform": "IPHONE"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "platform": "IPHONE"}, "emitted_at": 1716550966454} +{"stream": "ads_audience_reports_by_platform", "data": {"metrics": {"real_time_conversion_rate": "0.00", "cpm": "4.37", "real_time_cost_per_conversion": "0.00", "cpc": "0.15", "placement_type": "Automatic Placement", "campaign_id": 1779923887578145, "mobile_app_id": "0", "tt_app_id": "0", "real_time_result_rate": "2.96", "dpa_target_audience_type": "-", "impressions": "135", "cost_per_result": "0.15", "ctr": "2.96", "real_time_result": "4", "cost_per_conversion": "0.00", "campaign_name": "UTM_PARAMSTraffic20231016173112", "tt_app_name": "0", "adgroup_name": "Ad group 20231016073545", "result_rate": "2.96", "spend": "0.59", "result": "4", "ad_name": "Video16974675945951_Whale, sea, electronica(859574)_2023-10-16 07:46:35", "promotion_type": "Website", "ad_text": "airbyte", "conversion_rate": "0.00", "clicks": "4", "conversion": "0", "adgroup_id": 1779923881029666, "real_time_cost_per_result": "0.15", "real_time_conversion": "0"}, "dimensions": {"stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "platform": "IPAD"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "platform": "IPAD"}, "emitted_at": 1716550966459} +{"stream": "ads_audience_reports_by_province", "data": {"metrics": {"conversion_rate": "0.00", "campaign_id": 1779923887578145, "promotion_type": "Website", "real_time_conversion": "0", "ad_name": "Video16974675946002_Heartwarming Atmosphere Pops with Piano Main(827850)_2023-10-16 07:46:35", "dpa_target_audience_type": "-", "adgroup_name": "Ad group 20231016073545", "mobile_app_id": "0", "spend": "0.00", "ad_text": "airbyte", "cpc": "0.00", "impressions": "1", "real_time_conversion_rate": "0.00", "placement_type": "Automatic Placement", "real_time_result": "0", "tt_app_id": "0", "real_time_cost_per_result": "0.00", "conversion": "0", "cost_per_result": "0.00", "real_time_cost_per_conversion": "0.00", "ctr": "0.00", "adgroup_id": 1779923881029666, "cost_per_conversion": "0.00", "clicks": "0", "campaign_name": "UTM_PARAMSTraffic20231016173112", "real_time_result_rate": "0.00", "result": "0", "result_rate": "0.00", "cpm": "0.00", "tt_app_name": "0"}, "dimensions": {"ad_id": 1779923894511665, "stat_time_day": "2023-10-16 00:00:00", "province_id": "4099753"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894511665, "province_id": "4099753"}, "emitted_at": 1716551128258} +{"stream": "ads_audience_reports_by_province", "data": {"metrics": {"conversion_rate": "0.00", "campaign_id": 1779923887578145, "promotion_type": "Website", "real_time_conversion": "0", "ad_name": "Video16974675946002_Heartwarming Atmosphere Pops with Piano Main(827850)_2023-10-16 07:46:35", "dpa_target_audience_type": "-", "adgroup_name": "Ad group 20231016073545", "mobile_app_id": "0", "spend": "0.00", "ad_text": "airbyte", "cpc": "0.00", "impressions": "5", "real_time_conversion_rate": "0.00", "placement_type": "Automatic Placement", "real_time_result": "0", "tt_app_id": "0", "real_time_cost_per_result": "0.00", "conversion": "0", "cost_per_result": "0.00", "real_time_cost_per_conversion": "0.00", "ctr": "0.00", "adgroup_id": 1779923881029666, "cost_per_conversion": "0.00", "clicks": "0", "campaign_name": "UTM_PARAMSTraffic20231016173112", "real_time_result_rate": "0.00", "result": "0", "result_rate": "0.00", "cpm": "0.00", "tt_app_name": "0"}, "dimensions": {"ad_id": 1779923894511665, "stat_time_day": "2023-10-16 00:00:00", "province_id": "6254926"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894511665, "province_id": "6254926"}, "emitted_at": 1716551128263} +{"stream": "ads_audience_reports_by_province", "data": {"metrics": {"conversion_rate": "0.00", "campaign_id": 1779923887578145, "promotion_type": "Website", "real_time_conversion": "0", "ad_name": "Video16974675945951_Whale, sea, electronica(859574)_2023-10-16 07:46:35", "dpa_target_audience_type": "-", "adgroup_name": "Ad group 20231016073545", "mobile_app_id": "0", "spend": "0.17", "ad_text": "airbyte", "cpc": "0.17", "impressions": "14", "real_time_conversion_rate": "0.00", "placement_type": "Automatic Placement", "real_time_result": "1", "tt_app_id": "0", "real_time_cost_per_result": "0.17", "conversion": "0", "cost_per_result": "0.17", "real_time_cost_per_conversion": "0.00", "ctr": "7.14", "adgroup_id": 1779923881029666, "cost_per_conversion": "0.00", "clicks": "1", "campaign_name": "UTM_PARAMSTraffic20231016173112", "real_time_result_rate": "7.14", "result": "1", "result_rate": "7.14", "cpm": "12.14", "tt_app_name": "0"}, "dimensions": {"ad_id": 1779923894506609, "stat_time_day": "2023-10-16 00:00:00", "province_id": "4099753"}, "stat_time_day": "2023-10-16 00:00:00", "ad_id": 1779923894506609, "province_id": "4099753"}, "emitted_at": 1716551128268} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/metadata.yaml b/airbyte-integrations/connectors/source-tiktok-marketing/metadata.yaml index a4e7151baff8..e6857389b3d3 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/metadata.yaml +++ b/airbyte-integrations/connectors/source-tiktok-marketing/metadata.yaml @@ -11,7 +11,7 @@ data: connectorSubtype: api connectorType: source definitionId: 4bfac00d-ce15-44ff-95b9-9e3c3e8fbd35 - dockerImageTag: 3.9.10 + dockerImageTag: 4.0.0 dockerRepository: airbyte/source-tiktok-marketing documentationUrl: https://docs.airbyte.com/integrations/sources/tiktok-marketing githubIssueLabel: source-tiktok-marketing @@ -24,6 +24,50 @@ data: enabled: true oss: enabled: true + releases: + breakingChanges: + 4.0.0: + message: + The source TikTok Marketing connector is being migrated from the Python CDK + to our declarative low-code CDK. Due to changes in the handling of state + format for incremental substreams, this migration constitutes a breaking + change for the following streams - ad_groups, ads, campaigns, creative_assets_images, creative_assets_videos, + *_reports_daily, *_reports_hourly, *_reports_by_country_daily, *_reports_by_platform_daily. + Also the schema for advertiser_ids stream was changed to use string type of advertiser_id + field as API docs declares it. + Users will need to reset source configuration, refresh the source schema and reset the impacted streams after upgrading. + For more information, see our migration documentation for source TikTok Marketing. + upgradeDeadline: "2024-07-15" + scopedImpact: + - scopeType: stream + impactedScopes: + - "advertiser_ids" + - "ad_group_audience_reports_by_country_daily" + - "ad_group_audience_reports_by_platform_daily" + - "ad_group_audience_reports_daily" + - "ad_groups" + - "ad_groups_reports_daily" + - "ad_groups_reports_hourly" + - "ads" + - "ads_audience_reports_by_country_daily" + - "ads_audience_reports_by_platform_daily" + - "ads_audience_reports_by_province_daily" + - "ads_audience_reports_daily" + - "ads_reports_daily" + - "ads_reports_hourly" + - "advertisers_audience_reports_by_country_daily" + - "advertisers_audience_reports_by_platform_daily" + - "advertisers_audience_reports_daily" + - "advertisers_reports_daily" + - "advertisers_reports_hourly" + - "campaigns" + - "campaigns_audience_reports_by_country_daily" + - "campaigns_audience_reports_by_platform_daily" + - "campaigns_audience_reports_daily" + - "campaigns_reports_daily" + - "campaigns_reports_hourly" + - "creative_assets_images" + - "creative_assets_videos" releaseStage: generally_available remoteRegistries: pypi: @@ -41,7 +85,7 @@ data: supportLevel: certified tags: - language:python - - cdk:python + - cdk:low-code connectorTestSuitesOptions: - suite: unitTests testSecrets: diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/poetry.lock b/airbyte-integrations/connectors/source-tiktok-marketing/poetry.lock index cede7db42349..b60e9274b31c 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/poetry.lock +++ b/airbyte-integrations/connectors/source-tiktok-marketing/poetry.lock @@ -1,31 +1,35 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "airbyte-cdk" -version = "0.80.0" +version = "1.8.0" description = "A framework for writing Airbyte Connectors." optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "airbyte_cdk-0.80.0-py3-none-any.whl", hash = "sha256:060e92323a73674fa4e9e2e4a1eb312b9b9d072c9bbe5fa28f54ef21cb4974f3"}, - {file = "airbyte_cdk-0.80.0.tar.gz", hash = "sha256:1383512a83917fecca5b24cea4c72aa5c561cf96dd464485fbcefda48fe574c5"}, + {file = "airbyte_cdk-1.8.0-py3-none-any.whl", hash = "sha256:ca23d7877005fe87ffc4a3a3de29ee55eed625d874eb59b49664b156f9ae9ee2"}, + {file = "airbyte_cdk-1.8.0.tar.gz", hash = "sha256:ac82fbfd6b650b7ed015900748e30fdd2a4c574caa54d1bcc03cb584a17f1533"}, ] [package.dependencies] -airbyte-protocol-models = "0.5.1" +airbyte-protocol-models = ">=0.9.0,<1.0" backoff = "*" cachetools = "*" +cryptography = ">=42.0.5,<43.0.0" Deprecated = ">=1.2,<1.3" -dpath = ">=2.0.1,<2.1.0" +dpath = ">=2.1.6,<3.0.0" genson = "1.2.2" isodate = ">=0.6.1,<0.7.0" Jinja2 = ">=3.1.2,<3.2.0" jsonref = ">=0.2,<0.3" jsonschema = ">=3.2.0,<3.3.0" +langchain_core = "0.1.42" pendulum = "<3.0.0" pydantic = ">=1.10.8,<2.0.0" +pyjwt = ">=2.8.0,<3.0.0" pyrate-limiter = ">=3.1.0,<3.2.0" python-dateutil = "*" +pytz = "2024.1" PyYAML = ">=6.0.1,<7.0.0" requests = "*" requests_cache = "*" @@ -34,17 +38,17 @@ wcmatch = "8.4" [package.extras] file-based = ["avro (>=1.11.2,<1.12.0)", "fastavro (>=1.8.0,<1.9.0)", "markdown", "pdf2image (==1.16.3)", "pdfminer.six (==20221105)", "pyarrow (>=15.0.0,<15.1.0)", "pytesseract (==0.3.10)", "unstructured.pytesseract (>=0.3.12)", "unstructured[docx,pptx] (==0.10.27)"] sphinx-docs = ["Sphinx (>=4.2,<4.3)", "sphinx-rtd-theme (>=1.0,<1.1)"] -vector-db-based = ["cohere (==4.21)", "langchain (==0.0.271)", "openai[embeddings] (==0.27.9)", "tiktoken (==0.4.0)"] +vector-db-based = ["cohere (==4.21)", "langchain (==0.1.16)", "openai[embeddings] (==0.27.9)", "tiktoken (==0.4.0)"] [[package]] name = "airbyte-protocol-models" -version = "0.5.1" +version = "0.12.2" description = "Declares the Airbyte Protocol." optional = false python-versions = ">=3.8" files = [ - {file = "airbyte_protocol_models-0.5.1-py3-none-any.whl", hash = "sha256:dfe84e130e51ce2ae81a06d5aa36f6c5ce3152b9e36e6f0195fad6c3dab0927e"}, - {file = "airbyte_protocol_models-0.5.1.tar.gz", hash = "sha256:7c8b16c7c1c7956b1996052e40585a3a93b1e44cb509c4e97c1ee4fe507ea086"}, + {file = "airbyte_protocol_models-0.12.2-py3-none-any.whl", hash = "sha256:1780db5b26285865b858d26502933def8e11919c9436ccf7b8b9cb0170b07c2a"}, + {file = "airbyte_protocol_models-0.12.2.tar.gz", hash = "sha256:b7c4d9a7c32c0691601c2b9416af090a858e126666e2c8c880d7a1798eb519f0"}, ] [package.dependencies] @@ -148,6 +152,70 @@ files = [ {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -258,6 +326,60 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "42.0.8" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:81d8a521705787afe7a18d5bfb47ea9d9cc068206270aad0b96a725022e18d2e"}, + {file = "cryptography-42.0.8-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:961e61cefdcb06e0c6d7e3a1b22ebe8b996eb2bf50614e89384be54c48c6b63d"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3ec3672626e1b9e55afd0df6d774ff0e953452886e06e0f1eb7eb0c832e8902"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e599b53fd95357d92304510fb7bda8523ed1f79ca98dce2f43c115950aa78801"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5226d5d21ab681f432a9c1cf8b658c0cb02533eece706b155e5fbd8a0cdd3949"}, + {file = "cryptography-42.0.8-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6b7c4f03ce01afd3b76cf69a5455caa9cfa3de8c8f493e0d3ab7d20611c8dae9"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:2346b911eb349ab547076f47f2e035fc8ff2c02380a7cbbf8d87114fa0f1c583"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad803773e9df0b92e0a817d22fd8a3675493f690b96130a5e24f1b8fabbea9c7"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2f66d9cd9147ee495a8374a45ca445819f8929a3efcd2e3df6428e46c3cbb10b"}, + {file = "cryptography-42.0.8-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d45b940883a03e19e944456a558b67a41160e367a719833c53de6911cabba2b7"}, + {file = "cryptography-42.0.8-cp37-abi3-win32.whl", hash = "sha256:a0c5b2b0585b6af82d7e385f55a8bc568abff8923af147ee3c07bd8b42cda8b2"}, + {file = "cryptography-42.0.8-cp37-abi3-win_amd64.whl", hash = "sha256:57080dee41209e556a9a4ce60d229244f7a66ef52750f813bfbe18959770cfba"}, + {file = "cryptography-42.0.8-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:dea567d1b0e8bc5764b9443858b673b734100c2871dc93163f58c46a97a83d28"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4783183f7cb757b73b2ae9aed6599b96338eb957233c58ca8f49a49cc32fd5e"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0608251135d0e03111152e41f0cc2392d1e74e35703960d4190b2e0f4ca9c70"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dc0fdf6787f37b1c6b08e6dfc892d9d068b5bdb671198c72072828b80bd5fe4c"}, + {file = "cryptography-42.0.8-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9c0c1716c8447ee7dbf08d6db2e5c41c688544c61074b54fc4564196f55c25a7"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fff12c88a672ab9c9c1cf7b0c80e3ad9e2ebd9d828d955c126be4fd3e5578c9e"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:cafb92b2bc622cd1aa6a1dce4b93307792633f4c5fe1f46c6b97cf67073ec961"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:31f721658a29331f895a5a54e7e82075554ccfb8b163a18719d342f5ffe5ecb1"}, + {file = "cryptography-42.0.8-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b297f90c5723d04bcc8265fc2a0f86d4ea2e0f7ab4b6994459548d3a6b992a14"}, + {file = "cryptography-42.0.8-cp39-abi3-win32.whl", hash = "sha256:2f88d197e66c65be5e42cd72e5c18afbfae3f741742070e3019ac8f4ac57262c"}, + {file = "cryptography-42.0.8-cp39-abi3-win_amd64.whl", hash = "sha256:fa76fbb7596cc5839320000cdd5d0955313696d9511debab7ee7278fc8b5c84a"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ba4f0a211697362e89ad822e667d8d340b4d8d55fae72cdd619389fb5912eefe"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:81884c4d096c272f00aeb1f11cf62ccd39763581645b0812e99a91505fa48e0c"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c9bb2ae11bfbab395bdd072985abde58ea9860ed84e59dbc0463a5d0159f5b71"}, + {file = "cryptography-42.0.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7016f837e15b0a1c119d27ecd89b3515f01f90a8615ed5e9427e30d9cdbfed3d"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5a94eccb2a81a309806027e1670a358b99b8fe8bfe9f8d329f27d72c094dde8c"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dec9b018df185f08483f294cae6ccac29e7a6e0678996587363dc352dc65c842"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:343728aac38decfdeecf55ecab3264b015be68fc2816ca800db649607aeee648"}, + {file = "cryptography-42.0.8-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:013629ae70b40af70c9a7a5db40abe5d9054e6f4380e50ce769947b73bf3caad"}, + {file = "cryptography-42.0.8.tar.gz", hash = "sha256:8d09d05439ce7baa8e9e95b07ec5b6c886f548deb7e0f69ef25f64b3bce842f2"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "deprecated" version = "1.2.14" @@ -277,13 +399,13 @@ dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"] [[package]] name = "dpath" -version = "2.0.8" +version = "2.2.0" description = "Filesystem-like pathing and searching for dictionaries" optional = false python-versions = ">=3.7" files = [ - {file = "dpath-2.0.8-py3-none-any.whl", hash = "sha256:f92f595214dd93a00558d75d4b858beee519f4cffca87f02616ad6cd013f3436"}, - {file = "dpath-2.0.8.tar.gz", hash = "sha256:a3440157ebe80d0a3ad794f1b61c571bef125214800ffdb9afc9424e8250fe9b"}, + {file = "dpath-2.2.0-py3-none-any.whl", hash = "sha256:b330a375ded0a0d2ed404440f6c6a715deae5313af40bbb01c8a41d891900576"}, + {file = "dpath-2.2.0.tar.gz", hash = "sha256:34f7e630dc55ea3f219e555726f5da4b4b25f2200319c8e6902c394258dd6a3e"}, ] [[package]] @@ -363,6 +485,31 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jsonpatch" +version = "1.33" +description = "Apply JSON-Patches (RFC 6902)" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, !=3.6.*" +files = [ + {file = "jsonpatch-1.33-py2.py3-none-any.whl", hash = "sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade"}, + {file = "jsonpatch-1.33.tar.gz", hash = "sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c"}, +] + +[package.dependencies] +jsonpointer = ">=1.9" + +[[package]] +name = "jsonpointer" +version = "3.0.0" +description = "Identify specific nodes in a JSON document (RFC 6901)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942"}, + {file = "jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef"}, +] + [[package]] name = "jsonref" version = "0.2" @@ -395,6 +542,44 @@ six = ">=1.11.0" format = ["idna", "jsonpointer (>1.13)", "rfc3987", "strict-rfc3339", "webcolors"] format-nongpl = ["idna", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "webcolors"] +[[package]] +name = "langchain-core" +version = "0.1.42" +description = "Building applications with LLMs through composability" +optional = false +python-versions = "<4.0,>=3.8.1" +files = [ + {file = "langchain_core-0.1.42-py3-none-any.whl", hash = "sha256:c5653ffa08a44f740295c157a24c0def4a753333f6a2c41f76bf431cd00be8b5"}, + {file = "langchain_core-0.1.42.tar.gz", hash = "sha256:40751bf60ea5d8e2b2efe65290db434717ee3834870c002e40e2811f09d814e6"}, +] + +[package.dependencies] +jsonpatch = ">=1.33,<2.0" +langsmith = ">=0.1.0,<0.2.0" +packaging = ">=23.2,<24.0" +pydantic = ">=1,<3" +PyYAML = ">=5.3" +tenacity = ">=8.1.0,<9.0.0" + +[package.extras] +extended-testing = ["jinja2 (>=3,<4)"] + +[[package]] +name = "langsmith" +version = "0.1.82" +description = "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform." +optional = false +python-versions = "<4.0,>=3.8.1" +files = [ + {file = "langsmith-0.1.82-py3-none-any.whl", hash = "sha256:9b3653e7d316036b0c60bf0bc3e280662d660f485a4ebd8e5c9d84f9831ae79c"}, + {file = "langsmith-0.1.82.tar.gz", hash = "sha256:c02e2bbc488c10c13b52c69d271eb40bd38da078d37b6ae7ae04a18bd48140be"}, +] + +[package.dependencies] +orjson = ">=3.9.14,<4.0.0" +pydantic = {version = ">=1,<3", markers = "python_full_version < \"3.12.4\""} +requests = ">=2,<3" + [[package]] name = "markupsafe" version = "2.1.5" @@ -464,15 +649,70 @@ files = [ {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] +[[package]] +name = "orjson" +version = "3.10.5" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.8" +files = [ + {file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"}, + {file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"}, + {file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"}, + {file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"}, + {file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"}, + {file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"}, + {file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"}, + {file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"}, + {file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"}, + {file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"}, + {file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"}, + {file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"}, + {file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"}, + {file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"}, + {file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"}, + {file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"}, + {file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"}, + {file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"}, + {file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"}, + {file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"}, + {file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"}, + {file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"}, + {file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"}, + {file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"}, + {file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"}, + {file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"}, +] + [[package]] name = "packaging" -version = "24.1" +version = "23.2" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] [[package]] @@ -551,6 +791,17 @@ files = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydantic" version = "1.10.17" @@ -610,6 +861,23 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pyjwt" +version = "2.8.0" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "PyJWT-2.8.0-py3-none-any.whl", hash = "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320"}, + {file = "PyJWT-2.8.0.tar.gz", hash = "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pyrate-limiter" version = "3.1.1" @@ -721,6 +989,17 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "pytzdata" version = "2020.1" @@ -888,6 +1167,21 @@ files = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +[[package]] +name = "tenacity" +version = "8.4.2" +description = "Retry code until it succeeds" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tenacity-8.4.2-py3-none-any.whl", hash = "sha256:9e6f7cf7da729125c7437222f8a522279751cdfbe6b67bfe64f75d3a348661b2"}, + {file = "tenacity-8.4.2.tar.gz", hash = "sha256:cd80a53a79336edba8489e767f729e4f391c896956b57140b5d7511a64bbd3ef"}, +] + +[package.extras] +doc = ["reno", "sphinx"] +test = ["pytest", "tornado (>=4.5)", "typeguard"] + [[package]] name = "timeout-decorator" version = "0.5.0" @@ -1047,4 +1341,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9,<3.12" -content-hash = "dae736ca2caa9569937a51240b46694cf4689a734092af252200e68ac2ea37a4" +content-hash = "13edf8f801099dcc8ac226445c7dd0b7e123abe1769efad9bcd729bb0a994d44" diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/pyproject.toml b/airbyte-integrations/connectors/source-tiktok-marketing/pyproject.toml index 5b740d69fe4d..6b5c4064aacf 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/pyproject.toml +++ b/airbyte-integrations/connectors/source-tiktok-marketing/pyproject.toml @@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",] build-backend = "poetry.core.masonry.api" [tool.poetry] -version = "3.9.10" +version = "4.0.0" name = "source-tiktok-marketing" description = "Source implementation for Tiktok Marketing." authors = [ "Airbyte ",] @@ -17,7 +17,7 @@ include = "source_tiktok_marketing" [tool.poetry.dependencies] python = "^3.9,<3.12" -airbyte-cdk = "0.80.0" +airbyte-cdk = "^1" [tool.poetry.scripts] source-tiktok-marketing = "source_tiktok_marketing.run:run" diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/advertiser_ids_partition_router.py b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/advertiser_ids_partition_router.py new file mode 100644 index 000000000000..f74710d7d2ff --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/advertiser_ids_partition_router.py @@ -0,0 +1,73 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +import json +from typing import Any, Iterable, Mapping + +import dpath.util +from airbyte_cdk.sources.declarative.partition_routers.substream_partition_router import SubstreamPartitionRouter +from airbyte_cdk.sources.declarative.types import StreamSlice + + +class MultipleAdvertiserIdsPerPartition(SubstreamPartitionRouter): + """ + Custom AdvertiserIdsPartitionRouter and AdvertiserIdPartitionRouter partition routers are used to get advertiser_ids + as slices for streams where it uses as request param. + + When using a sandbox account, it's impossible to get advertiser_ids via API. + In this case user need to provide advertiser_id in a config and connector need to use provided ids + and do not make requests to get this id. + + When advertiser_id not provided components get slices as usual. + Main difference between AdvertiserIdsPartitionRouter and AdvertiserIdPartitionRouter is + that MultipleAdvertiserIdsPerPartition returns multiple advertiser_ids in a one slice when id is not provided, + e.g. {"advertiser_ids": '["11111111", "22222222"]', "parent_slice": {}}. + And SingleAdvertiserIdPerPartition returns single slice for every advertiser_id as usual. + + MultipleAdvertiserIdsPerPartition is used by Advertisers stream, which is full refresh only, where advertiser_ids is required param, + this approach also helps make less amount of requests. + Advertisers docs: https://business-api.tiktok.com/portal/docs?id=1739593083610113. + + path_in_config: List[List[str]]: path to value in the config in priority order. + partition_field: str: field to insert partition value. + """ + + def __post_init__(self, parameters: Mapping[str, Any]) -> None: + super().__post_init__(parameters) + self._path_to_partition_in_config = self._parameters["path_in_config"] + self._partition_field = self._parameters["partition_field"] + + def get_partition_value_from_config(self) -> str: + for path in self._path_to_partition_in_config: + config_value = dpath.util.get(self.config, path, default=None) + if config_value: + return config_value + + def stream_slices(self) -> Iterable[StreamSlice]: + partition_value_in_config = self.get_partition_value_from_config() + if partition_value_in_config: + slices = [partition_value_in_config] + else: + slices = [_id.partition[self._partition_field] for _id in super().stream_slices()] + + start, end, step = 0, len(slices), 100 + + for i in range(start, end, step): + yield StreamSlice(partition={"advertiser_ids": json.dumps(slices[i : min(end, i + step)]), "parent_slice": {}}, cursor_slice={}) + + +class SingleAdvertiserIdPerPartition(MultipleAdvertiserIdsPerPartition): + """ + SingleAdvertiserIdPerPartition returns single slice for every advertiser_id in the parent stream + or takes value for advertiser_id from a config and skips reading slices. + + path_in_config: List[List[str]]: path to value in the config in priority order. + partition_field: str: field to insert partition value. + """ + + def stream_slices(self) -> Iterable[StreamSlice]: + partition_value_in_config = self.get_partition_value_from_config() + + if partition_value_in_config: + yield StreamSlice(partition={self._partition_field: partition_value_in_config, "parent_slice": {}}, cursor_slice={}) + else: + yield from super(MultipleAdvertiserIdsPerPartition, self).stream_slices() diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/hourly_datetime_based_cursor.py b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/hourly_datetime_based_cursor.py new file mode 100644 index 000000000000..27ad24de6061 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/hourly_datetime_based_cursor.py @@ -0,0 +1,31 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +from typing import Iterable + +from airbyte_cdk.sources.declarative.incremental import DatetimeBasedCursor +from airbyte_cdk.sources.types import StreamSlice + + +class HourlyDatetimeBasedCursor(DatetimeBasedCursor): + """ + We need to overwrite stream_slices to replace hour=0, minute=0, second=0, microsecond=0 in start value in slices + because it is used as request params. + TikTok Marketing API doesn't allow date range more than one day. + In case when start date 2024-01-01 10:00:00 with step P1D slices look like {'start_time': '2024-01-01', 'end_time': '2024-01-02'}, + so API returns the following error: + 'code': 40002, 'message': 'max time span is 1 day when use stat_time_hour'. + To avoid such cases we replace start hours/minutes/seconds to 0 to have correct request params. + """ + + def stream_slices(self) -> Iterable[StreamSlice]: + """ + Partition the daterange into slices of size = step. + + The start of the window is the minimum datetime between start_datetime - lookback_window and the stream_state's datetime + The end of the window is the minimum datetime between the start of the window and end_datetime. + + :return: + """ + end_datetime = self.select_best_end_datetime() + start_datetime = self._calculate_earliest_possible_value(self.select_best_end_datetime()).replace(hour=0, minute=0, second=0) + return self._partition_daterange(start_datetime, end_datetime, self._step) diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/semi_incremental_record_filter.py b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/semi_incremental_record_filter.py new file mode 100644 index 000000000000..628a57a7a89f --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/semi_incremental_record_filter.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +from typing import Any, Iterable, Mapping, Optional + +from airbyte_cdk.sources.declarative.extractors import RecordFilter +from airbyte_cdk.sources.declarative.types import StreamSlice, StreamState + + +class PerPartitionRecordFilter(RecordFilter): + + """ + Prepares per partition stream state to be used in the Record Filter condition. + Gets current stream state cursor value for stream slice and passes it to condition. + + From + {"states": [{"partition": {"advertiser_id": 1, "parent_slice": {}}, "cursor": {"start_time": "2023-12-31"}}]} + To + {"start_time": "2023-12-31"}. + + partition_field:str: Used to get partition value from current slice. + """ + + def __post_init__(self, parameters: Mapping[str, Any]) -> None: + super().__post_init__(parameters) + self._partition_field = parameters["partition_field"] + + def filter_records( + self, + records: Iterable[Mapping[str, Any]], + stream_state: StreamState, + stream_slice: Optional[StreamSlice] = None, + next_page_token: Optional[Mapping[str, Any]] = None, + ) -> Iterable[Mapping[str, Any]]: + stream_state = next( + ( + p["cursor"] + for p in stream_state.get("states", []) + if p["partition"][self._partition_field] == stream_slice[self._partition_field] + ), + {}, + ) + + kwargs = {"stream_state": stream_state, "stream_slice": stream_slice, "next_page_token": next_page_token} + + for record in records: + if self._filter_interpolator.eval(self.config, record=record, **kwargs): + yield record diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/transformations.py b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/transformations.py new file mode 100644 index 000000000000..0b6883e6d13d --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/components/transformations.py @@ -0,0 +1,26 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +from dataclasses import InitVar, dataclass +from typing import Any, Mapping, Optional + +from airbyte_cdk.sources.declarative.transformations import RecordTransformation +from airbyte_cdk.sources.declarative.types import Config, FieldPointer, StreamSlice, StreamState + + +@dataclass +class TransformEmptyMetrics(RecordTransformation): + empty_value = "-" + + def transform( + self, + record: Mapping[str, Any], + config: Optional[Config] = None, + stream_state: Optional[StreamState] = None, + stream_slice: Optional[StreamSlice] = None, + ) -> Mapping[str, Any]: + + for metric_key, metric_value in record.get("metrics", {}).items(): + if metric_value == self.empty_value: + record["metrics"][metric_key] = None + + return record diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/manifest.yaml b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/manifest.yaml new file mode 100644 index 000000000000..25d2e0eefc39 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/manifest.yaml @@ -0,0 +1,4267 @@ +version: 1.1.0 +type: DeclarativeSource +check: + type: CheckStream + stream_names: + - advertisers + +definitions: + authenticator: + type: ApiKeyAuthenticator + api_token: "{{ config['credentials']['access_token'] if config.get('credentials') else config['access_token'] }}" + inject_into: + type: RequestOption + inject_into: header + field_name: Access-Token + + requester: + type: HttpRequester + url_base: '"https://{{ "sandbox-ads" if config.get(''credentials'', {}).get(''auth_type'', "") == "sandbox_access_token" else "business-api" }}.tiktok.com/open_api/v1.3/"' + path: "{{ parameters['path'] }}" + http_method: GET + error_handler: + type: DefaultErrorHandler + response_filters: + - predicate: "{{ response.get('code') != 0 }}" + action: FAIL + error_message: "{{ response['message'] }}" + authenticator: + $ref: "#/definitions/authenticator" + request_body_json: {} + + record_selector: + type: RecordSelector + schema_normalization: Default + extractor: + type: DpathExtractor + field_path: ["data", "list"] + + record_selector_with_filter_by_modify_time: + $ref: "#/definitions/record_selector" + record_filter: + type: CustomRecordFilter + class_name: "source_tiktok_marketing.components.semi_incremental_record_filter.PerPartitionRecordFilter" + condition: "{{ record['modify_time'] >= stream_state.get('modify_time', config.get('start_date', '')) }}" + $parameters: + partition_field: advertiser_id + + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_headers: {} + authenticator: + $ref: "#/definitions/authenticator" + request_body_json: {} + record_selector: + $ref: "#/definitions/requester" + paginator: + type: NoPagination + partition_router: [] + + paginator_page_increment: + type: "DefaultPaginator" + page_size_option: + type: "RequestOption" + inject_into: "request_parameter" + field_name: "page_size" + pagination_strategy: + type: "PageIncrement" + page_size: 100 + start_from_page: 1 + page_token_option: + type: "RequestOption" + inject_into: "request_parameter" + field_name: "page" + + incremental_sync: + type: DatetimeBasedCursor + cursor_field: "modify_time" + cursor_datetime_formats: + - "%Y-%m-%d %H:%M:%S" + - "%Y-%m-%dT%H:%M:%SZ" + datetime_format: "%Y-%m-%d %H:%M:%SZ" + start_datetime: + type: MinMaxDatetime + datetime: "{{ config.get('start_date', '2016-09-01') }}" + datetime_format: "%Y-%m-%d" + + single_id_partition_router: + - class_name: "source_tiktok_marketing.components.advertiser_ids_partition_router.SingleAdvertiserIdPerPartition" + $parameters: + path_in_config: + - ["credentials", "advertiser_id"] + - ["environment", "advertiser_id"] + partition_field: advertiser_id + parent_stream_configs: + - type: ParentStreamConfig + parent_key: advertiser_id + request_option: + inject_into: request_parameter + type: RequestOption + field_name: advertiser_id + partition_field: advertiser_id + stream: + $ref: "#/definitions/advertiser_ids_stream" + + multiple_id_partition_router: + - class_name: "source_tiktok_marketing.components.advertiser_ids_partition_router.MultipleAdvertiserIdsPerPartition" + $parameters: + path_in_config: + - ["credentials", "advertiser_id"] + - ["environment", "advertiser_id"] + partition_field: advertiser_ids + parent_stream_configs: + - type: ParentStreamConfig + parent_key: advertiser_id + request_option: + inject_into: request_parameter + type: RequestOption + field_name: advertiser_ids + partition_field: advertiser_ids + stream: + $ref: "#/definitions/advertiser_ids_stream" + + incremental_stream: + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + $ref: "#/definitions/record_selector_with_filter_by_modify_time" + paginator: + $ref: "#/definitions/paginator_page_increment" + pagination_strategy: + type: "PageIncrement" + page_size: '{{ parameters.get("page_size", 1000) }}' + start_from_page: 1 + partition_router: + $ref: "#/definitions/single_id_partition_router" + incremental_sync: + $ref: "#/definitions/incremental_sync" + + advertiser_ids_stream: + type: DeclarativeStream + name: advertiser_ids + $parameters: + path: "oauth2/advertiser/get/" + primary_key: + - advertiser_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/advertiser_ids" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + secret: "{{ config.get('credentials', config.get('environment', {})).get('secret') }}" + app_id: "{{ config.get('credentials', config.get('environment', {})).get('app_id', 0) }}" + request_headers: {} + record_selector: + $ref: "#/definitions/record_selector" + paginator: + type: NoPagination + partition_router: [] + + advertisers_stream: + type: DeclarativeStream + name: advertisers + $parameters: + path: "advertiser/info/" + primary_key: + - advertiser_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/advertisers" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + $ref: "#/definitions/record_selector" + paginator: + $ref: "#/definitions/paginator_page_increment" + partition_router: + $ref: "#/definitions/multiple_id_partition_router" + + audiences_stream: + type: DeclarativeStream + name: audiences + $parameters: + path: "dmp/custom_audience/list/" + primary_key: + - audience_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/audiences" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + $ref: "#/definitions/record_selector" + paginator: + $ref: "#/definitions/paginator_page_increment" + partition_router: + $ref: "#/definitions/single_id_partition_router" + + creative_assets_music_stream: + type: DeclarativeStream + name: creative_assets_music + $parameters: + path: "file/music/get/" + primary_key: + - music_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/creative_assets_music" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + type: RecordSelector + schema_normalization: Default + extractor: + type: DpathExtractor + field_path: ["data", "musics"] + paginator: + $ref: "#/definitions/paginator_page_increment" + partition_router: + $ref: "#/definitions/single_id_partition_router" + + creative_assets_portfolios_stream: + type: DeclarativeStream + name: creative_assets_portfolios + $parameters: + path: "creative/portfolio/list/" + primary_key: + - creative_portfolio_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/creative_assets_portfolios" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + record_selector: + type: RecordSelector + schema_normalization: Default + extractor: + type: DpathExtractor + field_path: ["data", "creative_portfolios"] + paginator: + $ref: "#/definitions/paginator_page_increment" + partition_router: + $ref: "#/definitions/single_id_partition_router" + + campaigns_stream: + type: DeclarativeStream + name: campaigns + $parameters: + path: "campaign/get/" + primary_key: + - campaign_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/campaigns" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + filtering: '{{ {"secondary_status": "CAMPAIGN_STATUS_ALL"}|string if config.get("include_deleted", False) }}' + record_selector: + $ref: "#/definitions/record_selector_with_filter_by_modify_time" + paginator: + $ref: "#/definitions/incremental_stream/retriever/paginator" + partition_router: + $ref: "#/definitions/single_id_partition_router" + incremental_sync: + $ref: "#/definitions/incremental_sync" + + ad_groups_stream: + type: DeclarativeStream + name: ad_groups + $parameters: + path: "adgroup/get/" + primary_key: + - adgroup_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/ad_groups" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + filtering: '{{ {"secondary_status": "ADGROUP_STATUS_ALL"}|string if config.get("include_deleted", False) }}' + record_selector: + $ref: "#/definitions/record_selector_with_filter_by_modify_time" + paginator: + $ref: "#/definitions/incremental_stream/retriever/paginator" + partition_router: + $ref: "#/definitions/single_id_partition_router" + incremental_sync: + $ref: "#/definitions/incremental_sync" + + ads_stream: + type: DeclarativeStream + name: ads + $parameters: + path: "ad/get/" + primary_key: + - ad_id + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/ads" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + filtering: '{{ {"secondary_status": "AD_STATUS_ALL"}|string if config.get("include_deleted", False) }}' + record_selector: + $ref: "#/definitions/record_selector_with_filter_by_modify_time" + paginator: + $ref: "#/definitions/incremental_stream/retriever/paginator" + partition_router: + $ref: "#/definitions/single_id_partition_router" + incremental_sync: + $ref: "#/definitions/incremental_sync" + + creative_assets_images_stream: + type: DeclarativeStream + name: creative_assets_images + $parameters: + path: "file/image/ad/search/" + page_size: 100 + primary_key: + - image_id + $ref: "#/definitions/incremental_stream" + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/creative_assets_images" + + creative_assets_videos_stream: + type: DeclarativeStream + name: creative_assets_videos + $parameters: + path: "file/video/ad/search/" + page_size: 100 + primary_key: + - video_id + $ref: "#/definitions/incremental_stream" + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/creative_assets_videos" + + record_selector_for_daily_reports_streams: + $ref: "#/definitions/record_selector" + record_filter: + type: CustomRecordFilter + class_name: "source_tiktok_marketing.components.semi_incremental_record_filter.PerPartitionRecordFilter" + condition: "{{ record['dimensions']['stat_time_day'] >= stream_state.get('stat_time_day', config.get('start_date', '')) }}" + $parameters: + partition_field: advertiser_id + + record_selector_for_hourly_reports_streams: + $ref: "#/definitions/record_selector" + record_filter: + type: CustomRecordFilter + class_name: "source_tiktok_marketing.components.semi_incremental_record_filter.PerPartitionRecordFilter" + condition: "{{ record['dimensions']['stat_time_hour'] >= stream_state.get('stat_time_hour', config.get('start_date', '')) }}" + $parameters: + partition_field: advertiser_id + + base_report_retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + service_type: "AUCTION" + report_type: "BASIC" + data_level: '{{ parameters["data_level"] }}' + dimensions: '{{ parameters["dimensions"] | string }}' + metrics: '{{ (parameters.get("report_metrics", []) + ["spend", "cpc", "cpm", "impressions", "clicks", "ctr", "reach", "cost_per_1000_reached", "frequency", "video_play_actions", "video_watched_2s", "video_watched_6s", "average_video_play", "average_video_play_per_user", "video_views_p25", "video_views_p50", "video_views_p75", "video_views_p100", "profile_visits", "likes", "comments", "shares", "follows", "clicks_on_music_disc", "real_time_app_install", "real_time_app_install_cost", "app_install"]) | string }}' + start_date: "{{ stream_interval['start_time'] }}" + end_date: "{{ stream_interval['end_time'] }}" + filters: '{{ [ + {"filter_value": ["STATUS_ALL"], "field_name": "ad_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "campaign_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "adgroup_status", "filter_type": "IN"}, + ] | string if config.get("include_deleted", False)}}' + authenticator: + $ref: "#/definitions/authenticator" + request_body_json: {} + record_selector: + $ref: "#/definitions/record_selector_for_daily_reports_streams" + paginator: + $ref: "#/definitions/paginator_page_increment" + pagination_strategy: + type: "PageIncrement" + page_size: 1000 + start_from_page: 1 + partition_router: + $ref: "#/definitions/single_id_partition_router" + + report_daily_incremental_sync: + type: DatetimeBasedCursor + cursor_field: "stat_time_day" + lookback_window: "P{{ config.get('attribution_window', 0) }}D" + cursor_granularity: "P1D" + step: P30D + cursor_datetime_formats: + - "%Y-%m-%d %H:%M:%S" + - "%Y-%m-%dT%H:%M:%SZ" + datetime_format: "%Y-%m-%d" + start_datetime: + type: MinMaxDatetime + datetime: "{{ config.get('start_date', '2016-09-01') }}" + datetime_format: "%Y-%m-%d" + end_datetime: + type: MinMaxDatetime + datetime: "{{ config.get('end_date', today_utc()) }}" + datetime_format: "%Y-%m-%d" + + report_hourly_incremental_sync: + type: CustomIncrementalSync + class_name: source_tiktok_marketing.components.hourly_datetime_based_cursor.HourlyDatetimeBasedCursor + cursor_field: "stat_time_hour" + lookback_window: "P{{ config.get('attribution_window', 0) }}D" + cursor_granularity: "PT1H" + step: P1D + cursor_datetime_formats: + - "%Y-%m-%d %H:%M:%S" + - "%Y-%m-%dT%H:%M:%SZ" + datetime_format: "%Y-%m-%d" + start_datetime: + type: MinMaxDatetime + datetime: "{{ config.get('start_date', '2016-09-01') }}" + datetime_format: "%Y-%m-%d" + end_datetime: + type: MinMaxDatetime + datetime: "{{ config.get('end_date', today_utc()) }}" + datetime_format: "%Y-%m-%d" + + base_report_daily: + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/base_report" + retriever: + $ref: "#/definitions/base_report_retriever" + record_selector: + $ref: "#/definitions/record_selector_for_daily_reports_streams" + incremental_sync: + $ref: "#/definitions/report_daily_incremental_sync" + + base_report_hourly: + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/base_report" + retriever: + $ref: "#/definitions/base_report_retriever" + record_selector: + $ref: "#/definitions/record_selector_for_hourly_reports_streams" + incremental_sync: + $ref: "#/definitions/report_hourly_incremental_sync" + + base_report_lifetime: + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/base_report" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + service_type: "AUCTION" + report_type: "BASIC" + data_level: '{{ parameters["data_level"] }}' + dimensions: '{{ parameters["dimensions"] | string }}' + metrics: '{{ (parameters.get("report_metrics", []) + ["spend", "cpc", "cpm", "impressions", "clicks", "ctr", "reach", "cost_per_1000_reached", "frequency", "video_play_actions", "video_watched_2s", "video_watched_6s", "average_video_play", "average_video_play_per_user", "video_views_p25", "video_views_p50", "video_views_p75", "video_views_p100", "profile_visits", "likes", "comments", "shares", "follows", "clicks_on_music_disc", "real_time_app_install", "real_time_app_install_cost", "app_install"]) | string }}' + query_lifetime: "true" + filters: '{{ [ + {"filter_value": ["STATUS_ALL"], "field_name": "ad_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "campaign_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "adgroup_status", "filter_type": "IN"}, + ] | string if config.get("include_deleted", False)}}' + paginator: + $ref: "#/definitions/paginator_page_increment" + pagination_strategy: + type: "PageIncrement" + page_size: 1000 + start_from_page: 1 + record_selector: + $ref: "#/definitions/record_selector" + partition_router: + $ref: "#/definitions/single_id_partition_router" + + ads_reports_daily_stream: + type: DeclarativeStream + name: ads_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + "total_purchase_value", + "total_onsite_shopping_value", + "onsite_shopping", + "vta_purchase", + "vta_conversion", + "cta_purchase", + "cta_conversion", + "total_pageview", + "complete_payment", + "value_per_complete_payment", + "total_complete_payment_rate", + ] + dimensions: ["ad_id", "stat_time_day"] + primary_key: + - ad_id + - stat_time_day + $ref: "#/definitions/base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ad_groups_reports_daily_stream: + type: DeclarativeStream + name: ad_groups_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADGROUP" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + ] + dimensions: ["adgroup_id", "stat_time_day"] + primary_key: + - adgroup_id + - stat_time_day + $ref: "#/definitions/base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["adgroup_id"] + value: "{{ record.dimensions.adgroup_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + advertisers_reports_daily_stream: + type: DeclarativeStream + name: advertisers_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + report_metrics: ["cash_spend", "voucher_spend"] + dimensions: ["advertiser_id", "stat_time_day"] + primary_key: + - advertiser_id + - stat_time_day + $ref: "#/definitions/base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + campaigns_reports_daily_stream: + type: DeclarativeStream + name: campaigns_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_CAMPAIGN" + report_metrics: ["campaign_name"] + dimensions: ["campaign_id", "stat_time_day"] + primary_key: + - campaign_id + - stat_time_day + $ref: "#/definitions/base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["campaign_id"] + value: "{{ record.dimensions.campaign_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + audience_base_report_retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + service_type: "AUCTION" + report_type: "AUDIENCE" + data_level: '{{ parameters["data_level"] }}' + dimensions: '{{ parameters["dimensions"] | string }}' + metrics: '{{ (parameters.get("report_metrics", []) + ["spend", "cpc", "cpm", "impressions", "clicks", "ctr"]) | string }}' + start_date: "{{ stream_interval['start_time'] }}" + end_date: "{{ stream_interval['end_time'] }}" + filters: '{{ [ + {"filter_value": ["STATUS_ALL"], "field_name": "ad_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "campaign_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "adgroup_status", "filter_type": "IN"}, + ] | string if config.get("include_deleted", False)}}' + authenticator: + $ref: "#/definitions/authenticator" + request_body_json: {} + record_selector: + $ref: "#/definitions/record_selector_for_daily_reports_streams" + paginator: + $ref: "#/definitions/paginator_page_increment" + pagination_strategy: + type: "PageIncrement" + page_size: 1000 + start_from_page: 1 + partition_router: + $ref: "#/definitions/single_id_partition_router" + + audience_base_report_daily: + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/audience_report" + retriever: + $ref: "#/definitions/audience_base_report_retriever" + incremental_sync: + $ref: "#/definitions/report_daily_incremental_sync" + + campaigns_audience_reports_daily_stream: + type: DeclarativeStream + name: campaigns_audience_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_CAMPAIGN" + report_metrics: ["campaign_name"] + dimensions: ["campaign_id", "stat_time_day", "gender", "age"] + primary_key: + - campaign_id + - stat_time_day + - gender + - age + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["campaign_id"] + value: "{{ record.dimensions.campaign_id }}" + - path: ["gender"] + value: "{{ record.dimensions.gender }}" + - path: ["age"] + value: "{{ record.dimensions.age }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ad_group_audience_reports_daily_stream: + type: DeclarativeStream + name: ad_group_audience_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADGROUP" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + ] + dimensions: ["adgroup_id", "stat_time_day", "gender", "age"] + primary_key: + - adgroup_id + - stat_time_day + - gender + - age + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["adgroup_id"] + value: "{{ record.dimensions.adgroup_id }}" + - path: ["gender"] + value: "{{ record.dimensions.gender }}" + - path: ["age"] + value: "{{ record.dimensions.age }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ads_audience_reports_daily_stream: + type: DeclarativeStream + name: ads_audience_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + ] + dimensions: ["ad_id", "stat_time_day", "gender", "age"] + primary_key: + - ad_id + - stat_time_day + - gender + - age + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - path: ["gender"] + value: "{{ record.dimensions.gender }}" + - path: ["age"] + value: "{{ record.dimensions.age }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + advertisers_audience_reports_daily_stream: + type: DeclarativeStream + name: advertisers_audience_reports_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + dimensions: ["advertiser_id", "stat_time_day", "gender", "age"] + primary_key: + - advertiser_id + - stat_time_day + - gender + - age + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - path: ["gender"] + value: "{{ record.dimensions.gender }}" + - path: ["age"] + value: "{{ record.dimensions.age }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + campaigns_audience_reports_by_country_daily_stream: + type: DeclarativeStream + name: campaigns_audience_reports_by_country_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_CAMPAIGN" + report_metrics: ["campaign_name"] + dimensions: ["campaign_id", "stat_time_day", "country_code"] + primary_key: + - campaign_id + - stat_time_day + - country_code + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["campaign_id"] + value: "{{ record.dimensions.campaign_id }}" + - path: ["country_code"] + value_type: "string" + value: "{{ record.dimensions.country_code }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ad_group_audience_reports_by_country_daily_stream: + type: DeclarativeStream + name: ad_group_audience_reports_by_country_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADGROUP" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + ] + dimensions: ["adgroup_id", "stat_time_day", "country_code"] + primary_key: + - adgroup_id + - stat_time_day + - country_code + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["adgroup_id"] + value: "{{ record.dimensions.adgroup_id }}" + - path: ["country_code"] + value_type: "string" + value: "{{ record.dimensions.country_code }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ads_audience_reports_by_country_daily_stream: + type: DeclarativeStream + name: ads_audience_reports_by_country_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + ] + dimensions: ["ad_id", "stat_time_day", "country_code"] + primary_key: + - ad_id + - stat_time_day + - country_code + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - path: ["country_code"] + value_type: "string" + value: "{{ record.dimensions.country_code }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + advertisers_audience_reports_by_country_daily_stream: + type: DeclarativeStream + name: advertisers_audience_reports_by_country_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + dimensions: ["advertiser_id", "stat_time_day", "country_code"] + primary_key: + - advertiser_id + - stat_time_day + - country_code + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - path: ["country_code"] + value_type: "string" + value: "{{ record.dimensions.country_code }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + campaigns_audience_reports_by_platform_daily_stream: + type: DeclarativeStream + name: campaigns_audience_reports_by_platform_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_CAMPAIGN" + report_metrics: ["campaign_name"] + dimensions: ["campaign_id", "stat_time_day", "platform"] + primary_key: + - campaign_id + - stat_time_day + - platform + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["campaign_id"] + value: "{{ record.dimensions.campaign_id }}" + - path: ["platform"] + value_type: "string" + value: "{{ record.dimensions.platform }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ad_group_audience_reports_by_platform_daily_stream: + type: DeclarativeStream + name: ad_group_audience_reports_by_platform_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADGROUP" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + ] + dimensions: ["adgroup_id", "stat_time_day", "platform"] + primary_key: + - adgroup_id + - stat_time_day + - platform + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["adgroup_id"] + value: "{{ record.dimensions.adgroup_id }}" + - path: ["platform"] + value_type: "string" + value: "{{ record.dimensions.platform }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ads_audience_reports_by_platform_daily_stream: + type: DeclarativeStream + name: ads_audience_reports_by_platform_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + ] + dimensions: ["ad_id", "stat_time_day", "platform"] + primary_key: + - ad_id + - stat_time_day + - platform + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - path: ["platform"] + value_type: "string" + value: "{{ record.dimensions.platform }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + advertisers_audience_reports_by_platform_daily_stream: + type: DeclarativeStream + name: advertisers_audience_reports_by_platform_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + dimensions: ["advertiser_id", "stat_time_day", "platform"] + primary_key: + - advertiser_id + - stat_time_day + - platform + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - path: ["platform"] + value_type: "string" + value: "{{ record.dimensions.platform }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ads_audience_reports_by_province_daily_stream: + type: DeclarativeStream + name: ads_audience_reports_by_province_daily + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + ] + dimensions: ["ad_id", "stat_time_day", "province_id"] + primary_key: + - ad_id + - stat_time_day + - province_id + $ref: "#/definitions/audience_base_report_daily" + transformations: + - type: AddFields + fields: + - path: ["stat_time_day"] + value: "{{ record.dimensions.stat_time_day }}" + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - path: ["province_id"] + value: "{{ record.dimensions.province_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ads_reports_hourly_stream: + type: DeclarativeStream + name: ads_reports_hourly + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + "total_purchase_value", + "total_onsite_shopping_value", + "onsite_shopping", + "vta_purchase", + "vta_conversion", + "cta_purchase", + "cta_conversion", + "total_pageview", + "complete_payment", + "value_per_complete_payment", + "total_complete_payment_rate", + ] + dimensions: ["ad_id", "stat_time_hour"] + primary_key: + - ad_id + - stat_time_hour + $ref: "#/definitions/base_report_hourly" + transformations: + - type: AddFields + fields: + - path: ["stat_time_hour"] + value: "{{ record.dimensions.stat_time_hour }}" + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + advertisers_reports_hourly_stream: + type: DeclarativeStream + name: advertisers_reports_hourly + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + dimensions: ["advertiser_id", "stat_time_hour"] + primary_key: + - advertiser_id + - stat_time_hour + $ref: "#/definitions/base_report_hourly" + transformations: + - type: AddFields + fields: + - path: ["stat_time_hour"] + value: "{{ record.dimensions.stat_time_hour }}" + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + campaigns_reports_hourly_stream: + type: DeclarativeStream + name: campaigns_reports_hourly + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_CAMPAIGN" + report_metrics: ["campaign_name"] + dimensions: ["campaign_id", "stat_time_hour"] + primary_key: + - campaign_id + - stat_time_hour + $ref: "#/definitions/base_report_hourly" + transformations: + - type: AddFields + fields: + - path: ["stat_time_hour"] + value: "{{ record.dimensions.stat_time_hour }}" + - path: ["campaign_id"] + value: "{{ record.dimensions.campaign_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ad_groups_reports_hourly_stream: + type: DeclarativeStream + name: ad_groups_reports_hourly + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADGROUP" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + ] + dimensions: ["adgroup_id", "stat_time_hour"] + primary_key: + - adgroup_id + - stat_time_hour + $ref: "#/definitions/base_report_hourly" + transformations: + - type: AddFields + fields: + - path: ["stat_time_hour"] + value: "{{ record.dimensions.stat_time_hour }}" + - path: ["adgroup_id"] + value: "{{ record.dimensions.adgroup_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ads_reports_lifetime_stream: + type: DeclarativeStream + name: ads_reports_lifetime + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_AD" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + "total_purchase_value", + "total_onsite_shopping_value", + "onsite_shopping", + "vta_purchase", + "vta_conversion", + "cta_purchase", + "cta_conversion", + "total_pageview", + "complete_payment", + "value_per_complete_payment", + "total_complete_payment_rate", + ] + dimensions: ["ad_id"] + primary_key: + - ad_id + $ref: "#/definitions/base_report_lifetime" + transformations: + - type: AddFields + fields: + - path: ["ad_id"] + value: "{{ record.dimensions.ad_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + advertisers_reports_lifetime_stream: + type: DeclarativeStream + name: advertisers_reports_lifetime + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + dimensions: ["advertiser_id"] + primary_key: + - advertiser_id + $ref: "#/definitions/base_report_lifetime" + transformations: + - type: AddFields + fields: + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + campaigns_reports_lifetime_stream: + type: DeclarativeStream + name: campaigns_reports_lifetime + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_CAMPAIGN" + report_metrics: ["campaign_name"] + dimensions: ["campaign_id"] + primary_key: + - campaign_id + $ref: "#/definitions/base_report_lifetime" + transformations: + - type: AddFields + fields: + - path: ["campaign_id"] + value: "{{ record.dimensions.campaign_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + ad_groups_reports_lifetime_stream: + type: DeclarativeStream + name: ad_groups_reports_lifetime + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADGROUP" + report_metrics: + [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + ] + dimensions: ["adgroup_id"] + primary_key: + - adgroup_id + $ref: "#/definitions/base_report_lifetime" + transformations: + - type: AddFields + fields: + - path: ["adgroup_id"] + value: "{{ record.dimensions.adgroup_id }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + audience_base_report_lifetime: + schema_loader: + type: InlineSchemaLoader + schema: + $ref: "#/definitions/schemas/audience_report" + retriever: + type: SimpleRetriever + requester: + $ref: "#/definitions/requester" + request_parameters: + service_type: "AUCTION" + report_type: "AUDIENCE" + data_level: '{{ parameters["data_level"] }}' + dimensions: '{{ parameters["dimensions"] | string }}' + metrics: '{{ (parameters.get("report_metrics", []) + ["spend", "cpc", "cpm", "impressions", "clicks", "ctr"]) | string }}' + start_date: '{{ day_delta(-365, "%Y-%m-%d") if config.get("start_date", "2016-09-01") < day_delta(-365, "%Y-%m-%d") else config["start_date"] }}' + end_date: "{{ today_utc() }}" + lifetime: "true" + filters: '{{ [ + {"filter_value": ["STATUS_ALL"], "field_name": "ad_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "campaign_status", "filter_type": "IN"}, + {"filter_value": ["STATUS_ALL"], "field_name": "adgroup_status", "filter_type": "IN"}, + ] | string if config.get("include_deleted", False)}}' + record_selector: + $ref: "#/definitions/record_selector" + paginator: + $ref: "#/definitions/paginator_page_increment" + pagination_strategy: + type: "PageIncrement" + page_size: 1000 + start_from_page: 1 + partition_router: + $ref: "#/definitions/single_id_partition_router" + + advertisers_audience_reports_lifetime_stream: + type: DeclarativeStream + name: advertisers_audience_reports_lifetime + $parameters: + path: "report/integrated/get/" + data_level: "AUCTION_ADVERTISER" + dimensions: ["advertiser_id", "gender", "age"] + primary_key: + - advertiser_id + - gender + - age + $ref: "#/definitions/audience_base_report_lifetime" + transformations: + - type: AddFields + fields: + - path: ["advertiser_id"] + value: "{{ record.dimensions.advertiser_id }}" + - path: ["gender"] + value: "{{ record.dimensions.gender }}" + - path: ["age"] + value: "{{ record.dimensions.age }}" + - type: CustomTransformation + class_name: "source_tiktok_marketing.components.transformations.TransformEmptyMetrics" + + schemas: + advertiser_ids: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + advertiser_id: + description: + The unique identifier for each advertiser in the TikTok marketing + platform. + type: string + advertiser_name: + description: The name of the advertiser registered in the TikTok marketing platform. + type: string + advertisers: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + advertiser_id: + description: Unique identifier for the advertiser. + type: integer + name: + description: The name of the advertiser or company. + type: string + address: + description: The physical address of the advertiser. + type: + - "null" + - string + company: + description: The name of the company associated with the advertiser. + type: + - "null" + - string + contacter: + description: The contact person for the advertiser. + type: + - "null" + - string + country: + description: The country where the advertiser is located. + type: + - "null" + - string + currency: + description: The currency used for transactions in the account. + type: + - "null" + - string + description: + description: A brief description or bio of the advertiser or company. + type: + - "null" + - string + email: + description: The email address associated with the advertiser. + type: + - "null" + - string + industry: + description: The industry or sector the advertiser operates in. + type: + - "null" + - string + language: + description: The preferred language of communication for the advertiser. + type: + - "null" + - string + license_no: + description: The license number of the advertiser. + type: + - "null" + - string + license_url: + description: The URL link to the advertiser's license documentation. + type: + - "null" + - string + cellphone_number: + description: The cellphone number of the advertiser. + type: + - "null" + - string + promotion_area: + description: The specific area or region where the advertiser focuses promotion. + type: + - "null" + - string + rejection_reason: + description: Reason for any advertisement rejection by the platform. + type: + - "null" + - string + role: + description: The role or position of the advertiser within the company. + type: + - "null" + - string + status: + description: The current status of the advertiser's account. + type: + - "null" + - string + timezone: + description: The timezone setting for the advertiser's activities. + type: + - "null" + - string + balance: + description: The current balance in the advertiser's account. + type: number + create_time: + description: The timestamp when the advertiser account was created. + type: integer + telephone_number: + description: The telephone number of the advertiser. + type: + - "null" + - string + display_timezone: + description: The timezone for display purposes. + type: + - "null" + - string + promotion_center_province: + description: + The province or state at the center of the advertiser's promotion + activities. + type: + - "null" + - string + advertiser_account_type: + description: The type of advertiser's account (e.g., individual, business). + type: + - "null" + - string + license_city: + description: The city where the advertiser's license is registered. + type: + - "null" + - string + brand: + description: The brand name associated with the advertiser. + type: + - "null" + - string + license_province: + description: The province or state where the advertiser's license is registered. + type: + - "null" + - string + promotion_center_city: + description: The city at the center of the advertiser's promotion activities. + type: + - "null" + - string + audiences: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + shared: + description: Flag indicating if the audience is shared with others + type: + - "null" + - boolean + is_creator: + description: Flag indicating if the audience creator is the user + type: + - "null" + - boolean + audience_id: + description: Unique identifier for the audience + type: + - "null" + - string + cover_num: + description: Number of audience members covered + type: + - "null" + - integer + create_time: + description: Timestamp indicating when the audience was created + type: + - "null" + - string + format: date-time + is_valid: + description: Flag indicating if the audience data is valid + type: + - "null" + - boolean + is_expiring: + description: Flag indicating if the audience data is expiring soon + type: + - "null" + - boolean + expired_time: + description: Timestamp indicating when the audience data expires + type: + - "null" + - string + format: date-time + name: + description: Name of the audience + type: + - "null" + - string + audience_type: + description: Type of audience (e.g., demographic, interest-based) + type: + - "null" + - string + calculate_type: + description: Method used to calculate audience data + type: + - "null" + - string + creative_assets_music: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + music_id: + description: The unique identifier for the music asset. + type: + - "null" + - string + material_id: + description: The unique ID assigned to the music asset. + type: + - "null" + - string + sources: + description: + The list of different sources or versions available for the music + asset. + type: + - "null" + - array + items: + type: + - "null" + - string + author: + description: The author of the music asset. + type: + - "null" + - string + liked: + description: The number of likes received by the music asset. + type: + - "null" + - boolean + cover_url: + description: The URL to the cover image associated with the music asset. + type: + - "null" + - string + url: + description: The URL to access or play the music asset. + type: + - "null" + - string + duration: + description: The duration of the music asset in seconds. + type: + - "null" + - number + style: + description: The style or genre of the music asset. + type: + - "null" + - string + signature: + description: The digital signature associated with the music asset. + type: + - "null" + - string + name: + description: The name or title of the music asset. + type: + - "null" + - string + file_name: + description: The file name of the music asset. + type: + - "null" + - string + copyright: + description: The copyright information related to the music asset. + type: + - "null" + - string + create_time: + description: The timestamp indicating when the music asset was created. + type: + - "null" + - string + format: date-time + modify_time: + description: The timestamp indicating when the music asset was last modified. + type: + - "null" + - string + format: date-time + creative_assets_portfolios: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + creative_portfolio_id: + description: The unique identifier for the creative portfolio. + type: + - "null" + - string + creative_portfolio_type: + description: The type of the creative portfolio, such as image, video, or carousel. + type: + - "null" + - string + creative_portfolio_preview_url: + description: The URL pointing to a preview image or video of the creative portfolio. + type: + - "null" + - string + campaigns: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + campaign_id: + description: The unique identifier of the campaign + type: integer + campaign_name: + description: Name of the campaign for easy identification + type: string + campaign_type: + description: Type of campaign (e.g., awareness, conversion) + type: string + advertiser_id: + description: The unique identifier of the advertiser associated with the campaign + type: integer + budget: + description: Total budget allocated for the campaign + type: number + budget_mode: + description: Mode in which the budget is being managed (e.g., daily, lifetime) + type: string + secondary_status: + description: Additional status information of the campaign + type: string + operation_status: + description: Current operational status of the campaign (e.g., active, paused) + type: + - "null" + - string + objective: + description: The objective or goal of the campaign + type: + - "null" + - string + objective_type: + description: + Type of objective selected for the campaign (e.g., brand awareness, + app installs) + type: + - "null" + - string + budget_optimize_on: + description: The metric or event that the budget optimization is based on + type: + - "null" + - boolean + bid_type: + description: Type of bid strategy being used in the campaign + type: + - "null" + - string + deep_bid_type: + description: Advanced bid type used for campaign optimization + type: + - "null" + - string + optimization_goal: + description: Specific goal to be optimized for in the campaign + type: + - "null" + - string + split_test_variable: + description: Variable being tested in a split test campaign + type: + - "null" + - string + is_new_structure: + description: Flag indicating if the campaign utilizes a new campaign structure + type: boolean + create_time: + description: Timestamp when the campaign was created + type: string + format: date-time + airbyte_type: timestamp_without_timezone + modify_time: + description: Timestamp when the campaign was last modified + type: string + format: date-time + airbyte_type: timestamp_without_timezone + roas_bid: + description: Return on ad spend goal set for the campaign + type: + - "null" + - number + is_smart_performance_campaign: + description: Flag indicating if the campaign uses smart performance optimization + type: + - "null" + - boolean + is_search_campaign: + description: Flag indicating if the campaign is a search campaign + type: + - "null" + - boolean + app_promotion_type: + description: Type of app promotion being used in the campaign + type: + - "null" + - string + rf_campaign_type: + description: Type of RF (reach and frequency) campaign being run + type: + - "null" + - string + ad_groups: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + adgroup_id: + description: The unique identifier of the ad group + type: integer + campaign_id: + description: The unique identifier of the campaign + type: integer + advertiser_id: + description: The unique identifier of the advertiser + type: integer + adgroup_name: + description: The name of the ad group + type: string + placement_type: + description: The type of ad placement + type: string + enum: + - PLACEMENT_TYPE_AUTOMATIC + - PLACEMENT_TYPE_NORMAL + placements: + description: Information about the ad placements targeted + type: + - "null" + - array + items: + type: string + inventory_filter_enabled: + description: Flag indicating if inventory filter is enabled + type: + - "null" + - boolean + comment_disabled: + description: Flag indicating if comments are disabled + type: boolean + app_id: + description: The unique identifier of the app + type: + - "null" + - integer + promotion_type: + description: The type of promotion + type: string + enum: + - APP_ANDROID + - APP_IOS + - WEBSITE + - LEAD_GENERATION + - WEBSITE_OR_DISPLAY + - TIKTOK_SHOP + - VIDEO_SHOPPING + - LIVE_SHOPPING + app_download_url: + description: The URL for downloading the associated app + type: + - "null" + - string + package: + description: The package used for the ad group + type: + - "null" + - string + pixel_id: + description: The ID of the pixel used for tracking + type: + - "null" + - integer + optimization_event: + description: The event used for optimization + type: + - "null" + - string + secondary_optimization_event: + description: Additional event used for optimization + type: + - "null" + - string + creative_material_mode: + description: The mode for creative materials + type: string + modify_time: + description: The timestamp for when the ad group was last modified + type: string + format: date-time + airbyte_type: timestamp_without_timezone + create_time: + description: The timestamp for when the ad group was created + type: string + format: date-time + airbyte_type: timestamp_without_timezone + audience_ids: + description: The IDs of the targeted audience + type: array + items: + type: integer + excluded_audience_ids: + description: The IDs of excluded audiences + type: array + items: + type: integer + audience_type: + description: The type of audience being targeted + type: + - "null" + - string + location_ids: + description: The IDs of targeted locations + type: array + items: + type: integer + is_hfss: + description: Flag indicating if high-frequency short sequences are included + type: boolean + interest_category_ids: + description: The IDs of interest categories for targeting + type: array + items: + type: integer + interest_keyword_ids: + description: The IDs of interest keywords for targeting + type: array + items: + type: integer + age_groups: + description: The targeted age groups for the ad group + type: + - "null" + - array + items: + type: string + gender: + description: The targeted gender for the ad group + type: + - "null" + - string + languages: + description: The targeted languages for the ad group + type: array + items: + type: string + operating_systems: + description: The targeted operating systems + type: array + items: + type: string + network_types: + description: The types of networks targeted + type: array + items: + type: string + device_price_ranges: + description: The price ranges for devices + type: + - "null" + - array + items: + type: number + min_android_version: + description: The minimum required Android version + type: + - "null" + - string + ios14_targeting: + description: Information about iOS 14 targeting settings + type: + - "null" + - string + device_model_ids: + description: The IDs of targeted device models + type: + - "null" + - array + items: + type: integer + min_ios_version: + description: The minimum required iOS version + type: + - "null" + - string + budget_mode: + description: The mode for managing the budget + type: string + budget: + description: The allocated budget for the ad group + type: number + schedule_type: + description: The type of scheduling + type: string + schedule_start_time: + description: The start time of the scheduling + type: string + format: date-time + airbyte_type: timestamp_without_timezone + schedule_end_time: + description: The end time of the scheduling + type: string + format: date-time + airbyte_type: timestamp_without_timezone + dayparting: + description: Information about dayparting settings + type: + - "null" + - string + optimization_goal: + description: The goal set for optimization + type: string + cpv_video_duration: + description: The duration for cost-per-view video + type: + - "null" + - string + conversion_window: + description: The window for tracking conversions + type: + - "null" + - string + pacing: + description: Information about the pacing settings + type: + - "null" + - string + billing_event: + description: The event used for billing + type: + - "null" + - string + skip_learning_phase: + description: Flag indicating if the learning phase is skipped + type: integer + bid_type: + description: The type of bidding + type: + - "null" + - string + bid_price: + description: The price set for bidding + type: number + conversion_bid_price: + description: The bid price for conversions + type: number + deep_bid_type: + description: The type of deep bid strategy + type: + - "null" + - string + deep_cpa_bid: + description: The bid amount for deep cost-per-action + type: number + secondary_status: + description: The secondary status of the ad group + type: string + operation_status: + description: The status of the operation + type: string + frequency: + description: The frequency of ad display + type: + - "null" + - integer + frequency_schedule: + description: The schedule for frequency capping + type: + - "null" + - integer + statistic_type: + description: The type of statistics being tracked + type: + - "null" + - string + carrier_ids: + description: The IDs of the targeted carriers + type: + - "null" + - array + items: + type: integer + carriers: + description: Information about the targeted carriers + type: + - "null" + - array + items: + type: string + video_download_disabled: + description: Flag indicating if video downloads are disabled + type: boolean + blocked_pangle_app_ids: + description: The IDs of the blocked Pangle apps + type: + - "null" + - array + items: + type: string + action_category_ids: + description: The IDs of the action categories associated with the ad group + type: + - "null" + - array + items: + type: string + action_days: + description: The number of days the action has been performed + type: + - "null" + - integer + video_actions: + description: Information about video-specific actions + type: + - "null" + - array + items: + type: string + rf_purchased_type: + description: Type of purchased results + type: + - "null" + - string + purchased_impression: + description: Information about purchased impressions + type: + - "null" + - number + purchased_reach: + description: Information about purchased reach + type: + - "null" + - number + rf_estimated_cpr: + description: Estimated cost per result + type: + - "null" + - number + rf_estimated_frequency: + description: Estimated frequency of results + type: + - "null" + - number + included_pangle_audience_package_ids: + description: The IDs of included Pangle audience packages + type: + - "null" + - array + items: + type: number + excluded_pangle_audience_package_ids: + description: The IDs of excluded Pangle audience packages + type: + - "null" + - array + items: + type: number + is_new_structure: + description: Flag indicating if the ad group follows a new structure + type: boolean + is_smart_performance_campaign: + description: Flag indicating if the campaign is using smart performance + type: + - "null" + - boolean + catalog_id: + description: The unique identifier of the catalog + type: + - "null" + - integer + product_set_id: + description: The ID of the product set + type: + - "null" + - integer + catalog_authorized_bc_id: + description: The authorized Business Center ID for the catalog + type: + - "null" + - integer + audience_rule: + description: The rule set for targeting the audience + type: + - "null" + - object + included_custom_actions: + description: Custom actions that are included + type: + - "null" + - array + items: + type: object + excluded_custom_actions: + description: Custom actions that are excluded + type: + - "null" + - array + items: + type: object + shopping_ads_retargeting_type: + description: The type of retargeting used for shopping ads + type: + - "null" + - string + split_test_adgroup_ids: + description: The IDs of ad groups participating in split testing + type: + - "null" + - array + items: + type: number + brand_safety_type: + description: The type of brand safety measures + type: + - "null" + - string + brand_safety_partner: + description: Information about the brand safety partners + type: + - "null" + - string + promotion_website_type: + description: The type of website used for promotion + type: + - "null" + - string + ios_quota_type: + description: The type of iOS quota + type: + - "null" + - string + roas_bid: + description: The bid amount set for return on ad spend + type: + - "null" + - number + actions: + description: Information about the actions taken on the ad group + type: + - "null" + - array + items: + type: + - "null" + - object + properties: + action_category_ids: + description: The IDs of the action categories for the specific action + type: + - "null" + - array + items: + type: integer + action_period: + description: The period during which the action was taken + type: + - "null" + - number + action_scene: + description: The scene in which the action took place + type: + - "null" + - string + video_user_actions: + description: User actions specific to video content + type: + - "null" + - array + items: + type: string + targeting_expansion: + description: Settings for targeting expansion + type: + - "null" + - object + properties: + expansion_enabled: + description: Flag indicating if targeting expansion is enabled + type: boolean + expansion_types: + description: Types of expansion enabled + type: + - "null" + - array + items: + type: string + schedule_infos: + description: Information about the scheduling arrangements + type: + - "null" + - array + items: + type: object + share_disabled: + description: Flag indicating if sharing is disabled + type: + - "null" + - boolean + auto_targeting_enabled: + description: Flag indicating if auto-targeting is enabled + type: + - "null" + - boolean + ios14_quota_type: + description: The type of iOS 14 quota + type: + - "null" + - string + campaign_name: + description: The name of the campaign + type: + - "null" + - string + bid_display_mode: + description: The display mode for bidding + type: + - "null" + - string + scheduled_budget: + description: The budget allocated for scheduling + type: + - "null" + - number + adgroup_app_profile_page_state: + description: The state of the app profile page related to the ad group + type: + - "null" + - string + keywords: + description: Keywords associated with the ad group + type: + - "null" + - string + next_day_retention: + description: Retention information for the next day + type: + - "null" + - number + category_id: + description: The ID of the category for the ad group + type: + - "null" + - integer + search_result_enabled: + description: Flag indicating if search results are enabled + type: + - "null" + - boolean + app_type: + description: The type of the associated app + type: + - "null" + - string + feed_type: + description: The type of feed used + type: + - "null" + - string + delivery_mode: + description: The mode for delivery + type: + - "null" + - string + category_exclusion_ids: + description: The IDs of the excluded categories + type: + - "null" + - array + items: + type: string + contextual_tag_ids: + description: The IDs of contextual tags for targeting + type: + - "null" + - array + items: + type: + - "null" + - string + household_income: + description: The targeted household income groups + type: + - "null" + - array + items: + type: + - "null" + - string + isp_ids: + description: The IDs of the targeted internet service providers + type: + - "null" + - array + items: + type: + - "null" + - string + spending_power: + description: Information about the spending power targeted + type: + - "null" + - string + zipcode_ids: + description: The IDs of targeted ZIP codes + type: + - "null" + - array + items: + type: + - "null" + - string + ads: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + advertiser_id: + description: The unique identifier of the advertiser + type: integer + campaign_id: + description: The unique identifier of the campaign + type: integer + campaign_name: + description: The name of the campaign + type: string + adgroup_id: + description: The unique identifier of the ad group + type: integer + adgroup_name: + description: The name of the ad group + type: string + ad_id: + description: The unique identifier of the ad + type: integer + ad_name: + description: The name of the ad + type: string + tracking_app_id: + description: The unique identifier of the tracking app + type: + - "null" + - string + tracking_offline_event_set_ids: + description: The unique identifiers of offline event sets for tracking + type: + - "null" + - array + items: + description: Unique identifier of an offline event set + type: + - "null" + - string + call_to_action: + description: The call-to-action text for the ad + type: + - "null" + - string + call_to_action_id: + description: The identifier of the call-to-action + type: + - "null" + - string + disclaimer_type: + description: The type of disclaimer displayed + type: + - "null" + - string + disclaimer_text: + description: The disclaimer text + type: + - "null" + - object + properties: + text: + description: The text of the disclaimer + type: + - "null" + - string + disclaimer_clickable_texts: + description: Clickable disclaimer texts with URLs + type: + - "null" + - object + properties: + text: + description: The disclaimer text + type: + - "null" + - string + url: + description: The URL associated with the disclaimer text + type: + - "null" + - string + card_id: + description: The identifier of the card + type: + - "null" + - integer + secondary_status: + description: The secondary status of the ad + type: string + operation_status: + description: The operational status of the ad + type: + - "null" + - string + is_aco: + description: Indicates if the ad is under Automated Creative Optimization + type: + - "null" + - boolean + image_ids: + description: The unique identifiers of images used in the ad + type: + - "null" + - array + items: + description: Unique identifier of an image + type: string + image_mode: + description: The mode of displaying images + type: + - "null" + - string + ad_format: + description: The format of the ad (e.g., image, video, carousel) + type: + - "null" + - string + ad_text: + description: The text content of the ad + type: + - "null" + - string + ad_texts: + description: The text content of the ad in various languages + type: + - "null" + - array + items: + description: Text content in a specific language + type: string + video_id: + description: The unique identifier of the video + type: + - "null" + - string + tiktok_item_id: + description: The unique identifier of the TikTok item + type: + - "null" + - string + premium_badge_id: + description: The unique identifier of the premium badge + type: + - "null" + - string + app_name: + description: The name of the mobile app where the ad is displayed + type: + - "null" + - string + landing_page_url: + description: The URL of the landing page for the ad + type: + - "null" + - string + landing_page_urls: + description: The URLs of landing pages for the ad + type: + - "null" + - array + items: + description: URL of a landing page + type: string + display_name: + description: The display name of the ad + type: + - "null" + - string + profile_image_url: + description: The URL of the profile image associated with the ad + type: + - "null" + - string + impression_tracking_url: + description: The URL for tracking ad impressions + type: + - "null" + - string + click_tracking_url: + description: The URL for tracking ad clicks + type: + - "null" + - string + tracking_pixel_id: + description: The unique identifier of the tracking pixel + type: + - "null" + - integer + deeplink: + description: The deeplink URL for the ad + type: + - "null" + - string + deeplink_type: + description: The type of deeplink used + type: + - "null" + - string + fallback_type: + description: The type of fallback used + type: + - "null" + - string + playable_url: + description: The URL for a playable ad + type: + - "null" + - string + vast_moat_enabled: + description: Indicates if VAST MOAT is enabled + type: + - "null" + - boolean + page_id: + description: The unique identifier of the page + type: + - "null" + - number + creative_authorized: + description: Indicates if the creative is authorized + type: + - "null" + - boolean + is_new_structure: + description: Indicates if the ad is part of a new structure + type: + - "null" + - boolean + create_time: + description: The timestamp when the ad was created + type: string + format: date-time + airbyte_type: timestamp_without_timezone + modify_time: + description: The timestamp when the ad was last modified + type: string + format: date-time + airbyte_type: timestamp_without_timezone + shopping_ads_fallback_type: + description: The type of fallback for shopping ads + type: + - "null" + - string + shopping_deeplink_type: + description: The type of deeplink for shopping + type: + - "null" + - string + shopping_ads_video_package_id: + description: The unique identifier of the video package for shopping ads + type: + - "null" + - string + promotional_music_disabled: + description: Indicates if promotional music is disabled + type: + - "null" + - boolean + item_duet_status: + description: The status of item duet + type: + - "null" + - string + item_stitch_status: + description: The status of item stitch + type: + - "null" + - string + avatar_icon_web_uri: + description: The URL of the avatar icon for the ad + type: + - "null" + - string + brand_safety_postbid_partner: + description: Details about post-bidding partner for brand safety + type: + - "null" + - string + brand_safety_vast_url: + description: The VAST URL for brand safety tracking + type: + - "null" + - string + creative_type: + description: The type of creative used in the ad + type: + - "null" + - string + identity_id: + description: The identifier of the identity + type: + - "null" + - string + identity_type: + description: The type of identity + type: + - "null" + - string + identity_authorized_bc_id: + description: The authorized identity for branded content + type: + - "null" + - string + phone_region_code: + description: The region code for the phone number + type: + - "null" + - string + phone_region_calling_code: + description: The calling code region for the phone number + type: + - "null" + - string + optimization_event: + description: The event used for optimization + type: + - "null" + - string + phone_number: + description: The phone number associated with the ad + type: + - "null" + - string + carousel_image_index: + description: The index of the image in a carousel ad + type: + - "null" + - integer + viewability_postbid_partner: + description: Details about post-bidding partner for viewability tracking + type: + - "null" + - string + viewability_vast_url: + description: The VAST URL for viewability tracking + type: + - "null" + - string + music_id: + description: The unique identifier of the music used in the ad + type: + - "null" + - string + utm_params: + description: UTM parameters for tracking + type: + - "null" + - array + items: + description: Key-value pair for a UTM parameter + type: + - "null" + - object + properties: + key: + description: The key of the UTM parameter + type: + - "null" + - string + value: + description: The value of the UTM parameter + type: + - "null" + - string + shopping_ads_deeplink_type: + description: The type of deeplink for shopping ads + type: + - "null" + - string + dark_post_status: + description: The status of dark post + type: + - "null" + - string + branded_content_disabled: + description: Indicates if branded content is disabled + type: + - "null" + - string + product_specific_type: + description: The specific type of product + type: + - "null" + - string + catalog_id: + description: The unique identifier of the catalog + type: + - "null" + - string + item_group_ids: + description: The unique identifiers of item groups + type: + - "null" + - array + items: + description: Unique identifier of an item group + type: + - "null" + - string + product_set_id: + description: The unique identifier of the product set + type: + - "null" + - string + sku_ids: + description: The unique identifiers of SKUs associated with the ad + type: + - "null" + - array + items: + description: Unique identifier of a SKU + type: + - "null" + - string + dynamic_format: + description: The dynamic format of the ad + type: + - "null" + - string + vertical_video_strategy: + description: The strategy for displaying vertical videos + type: + - "null" + - string + dynamic_destination: + description: The dynamic destination of the ad + type: + - "null" + - string + showcase_products: + description: Products displayed in a showcase ad + type: + - "null" + - object + properties: + item_group_id: + description: The unique identifier of the item group + type: + - "null" + - string + store_id: + description: The unique identifier of the store + type: + - "null" + - string + catalog_id: + description: The unique identifier of the catalog + type: + - "null" + - string + tiktok_page_category: + description: The category of the TikTok page + type: + - "null" + - string + creative_assets_images: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + image_id: + description: The unique identifier for the image. + type: + - "null" + - string + format: + description: The format type of the image file. + type: + - "null" + - string + image_url: + description: The URL to access the image. + type: + - "null" + - string + height: + description: The height dimension of the image. + type: + - "null" + - integer + width: + description: The width dimension of the image. + type: + - "null" + - integer + signature: + description: The signature of the image for security purposes. + type: + - "null" + - string + size: + description: The size of the image file. + type: + - "null" + - integer + material_id: + description: The ID associated with the material of the image. + type: + - "null" + - string + is_carousel_usable: + description: Flag indicating if the image can be used in a carousel. + type: + - "null" + - boolean + file_name: + description: The name of the image file. + type: + - "null" + - string + create_time: + description: The timestamp when the creative asset image was created. + type: + - "null" + - string + format: date-time + modify_time: + description: The timestamp when the creative asset image was last modified. + type: + - "null" + - string + format: date-time + displayable: + description: Flag indicating if the image is displayable or not. + type: + - "null" + - boolean + creative_assets_videos: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + video_id: + description: ID of the video. + type: + - "null" + - string + video_cover_url: + description: URL for the cover image of the video. + type: + - "null" + - string + format: + description: Format of the video file. + type: + - "null" + - string + preview_url: + description: URL for previewing the video. + type: + - "null" + - string + preview_url_expire_time: + description: Timestamp when the preview URL expires. + type: + - "null" + - string + format: date-time + airbyte_type: timestamp_without_timezone + duration: + description: Duration of the video in seconds. + type: + - "null" + - number + height: + description: Height of the video in pixels. + type: + - "null" + - integer + width: + description: Width of the video in pixels. + type: + - "null" + - integer + bit_rate: + description: The bitrate of the video. + type: + - "null" + - number + signature: + description: Signature for authenticating the video request. + type: + - "null" + - string + size: + description: Size of the video file in bytes. + type: + - "null" + - integer + material_id: + description: ID of the video material. + type: + - "null" + - string + allowed_placements: + description: List of placements where the video can be used. + type: + - "null" + - array + items: + description: Specific placement where the video is allowed. + type: + - "null" + - string + allow_download: + description: Indicates if the video can be downloaded by users. + type: + - "null" + - boolean + file_name: + description: Name of the video file. + type: + - "null" + - string + create_time: + description: Timestamp when the video was created. + type: + - "null" + - string + format: date-time + modify_time: + description: Timestamp when the video was last modified. + type: + - "null" + - string + format: date-time + displayable: + description: Indicates if the video is displayable. + type: + - "null" + - boolean + base_report: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + stat_time_day: + description: The date for which the statistical data is recorded. + type: + - "null" + - string + format: date-time + airbyte_type: timestamp_without_timezone + stat_time_hour: + description: The hour of the day for which the statistical data is recorded. + type: + - "null" + - string + format: date-time + airbyte_type: timestamp_without_timezone + campaign_id: + description: The unique identifier for a marketing campaign. + type: + - "null" + - integer + adgroup_id: + description: The unique identifier for an ad group. + type: + - "null" + - integer + ad_id: + description: The unique identifier for an advertisement. + type: + - "null" + - integer + advertiser_id: + description: The unique identifier for an advertiser. + type: + - "null" + - integer + metrics: + description: + A list of metrics for which data should be retrieved such as views, + likes, comments, or shares. + type: + - "null" + - object + properties: + campaign_name: + description: The name of the marketing campaign. + type: + - "null" + - string + campaign_id: + description: + The unique identifier for a marketing campaign within the metrics + level. + type: + - "null" + - integer + adgroup_name: + description: The name of the ad group. + type: + - "null" + - string + placement_type: + description: Type of advertisement placement. + type: + - "null" + - string + adgroup_id: + description: The unique identifier for an ad group within the metrics level. + type: + - "null" + - integer + ad_name: + description: The name of the advertisement. + type: + - "null" + - string + ad_text: + description: The content or text of the advertisement. + type: + - "null" + - string + tt_app_id: + description: The unique identifier for a TikTok application. + type: + - "null" + - integer + tt_app_name: + description: The name of the TikTok application. + type: + - "null" + - string + mobile_app_id: + description: The unique identifier for a mobile application. + type: + - "null" + - string + promotion_type: + description: Type of promotion. + type: + - "null" + - string + dpa_target_audience_type: + description: Dynamic product ad target audience type. + type: + - "null" + - string + spend: + description: Amount of money spent. + type: + - "null" + - string + cash_spend: + description: The amount of money spent in cash. + type: + - "null" + - string + voucher_spend: + description: Amount spent on vouchers. + type: + - "null" + - string + cpc: + description: Cost per click. + type: + - "null" + - string + cpm: + description: Cost per thousand impressions. + type: + - "null" + - string + impressions: + description: Number of times the advertisement is viewed. + type: + - "null" + - string + clicks: + description: The number of clicks on the advertisement. + type: + - "null" + - string + ctr: + description: Click-through rate. + type: + - "null" + - string + reach: + description: Total number of unique users reached. + type: + - "null" + - string + cost_per_1000_reached: + description: The cost per 1000 reached users. + type: + - "null" + - string + conversion: + description: The number of conversions. + type: + - "null" + - string + cost_per_conversion: + description: The cost per conversion. + type: + - "null" + - string + conversion_rate: + description: The rate of conversion. + type: + - "null" + - string + real_time_conversion: + description: Real-time conversions. + type: + - "null" + - string + real_time_cost_per_conversion: + description: Cost per conversion in real-time. + type: + - "null" + - string + real_time_conversion_rate: + description: Real-time conversion rate. + type: + - "null" + - string + result: + description: Number of results. + type: + - "null" + - string + cost_per_result: + description: The cost per result. + type: + - "null" + - string + result_rate: + description: Rate of results. + type: + - "null" + - string + real_time_result: + description: Real-time results. + type: + - "null" + - string + real_time_cost_per_result: + description: Cost per result in real-time. + type: + - "null" + - string + real_time_result_rate: + description: Real-time result rate. + type: + - "null" + - string + secondary_goal_result: + description: Results for secondary goals. + type: + - "null" + - string + cost_per_secondary_goal_result: + description: The cost per secondary goal result. + type: + - "null" + - string + secondary_goal_result_rate: + description: Rate of results for secondary goals. + type: + - "null" + - string + frequency: + description: Frequency of occurrence. + type: + - "null" + - string + total_purchase_value: + description: Total value of purchases made. + type: + - "null" + - string + total_onsite_shopping_value: + description: Total value of onsite shopping. + type: + - "null" + - string + onsite_shopping: + description: Shopping happening on the site. + type: + - "null" + - string + vta_purchase: + description: Purchase through vertical takeoff ad (VTA). + type: + - "null" + - string + cta_purchase: + description: Purchase through call-to-action. + type: + - "null" + - string + cta_conversion: + description: Conversion through call-to-action. + type: + - "null" + - string + vta_conversion: + description: Conversion through vertical takeoff ad (VTA). + type: + - "null" + - string + total_pageview: + description: Total number of page views. + type: + - "null" + - string + complete_payment: + description: The number of completed payments. + type: + - "null" + - string + value_per_complete_payment: + description: Value per completed payment. + type: + - "null" + - string + total_complete_payment_rate: + description: Rate of total completed payments. + type: + - "null" + - string + video_play_actions: + description: Actions related to video plays. + type: + - "null" + - number + video_watched_2s: + description: Number of viewers watching at least 2 seconds of the video. + type: + - "null" + - number + video_watched_6s: + description: Number of viewers watching at least 6 seconds of the video. + type: + - "null" + - number + average_video_play: + description: The average number of video plays. + type: + - "null" + - number + average_video_play_per_user: + description: The average number of video plays per user. + type: + - "null" + - number + video_views_p25: + description: Percentage of viewers watching at least 25% of the video. + type: + - "null" + - number + video_views_p50: + description: Percentage of viewers watching at least 50% of the video. + type: + - "null" + - number + video_views_p75: + description: Percentage of viewers watching at least 75% of the video. + type: + - "null" + - number + video_views_p100: + description: Percentage of viewers watching the entire video. + type: + - "null" + - number + profile_visits: + description: Number of visits to the profile. + type: + - "null" + - number + likes: + description: Number of likes received. + type: + - "null" + - number + comments: + description: The number of comments received. + type: + - "null" + - number + shares: + description: Number of shares. + type: + - "null" + - number + follows: + description: Number of follows. + type: + - "null" + - number + clicks_on_music_disc: + description: The number of clicks on the music disc. + type: + - "null" + - number + real_time_app_install: + description: Real-time app installations. + type: + - "null" + - number + real_time_app_install_cost: + description: Cost of real-time app installations. + type: + - "null" + - number + app_install: + description: The number of app installations. + type: + - "null" + - number + profile_visits_rate: + description: Rate of profile visits. + type: + - "null" + - number + purchase: + description: Number of purchases made. + type: + - "null" + - number + purchase_rate: + description: Rate of purchases. + type: + - "null" + - number + registration: + description: Number of registrations. + type: + - "null" + - number + registration_rate: + description: Rate of registrations. + type: + - "null" + - number + sales_lead: + description: Number of sales leads. + type: + - "null" + - number + sales_lead_rate: + description: Rate of sales leads. + type: + - "null" + - number + cost_per_app_install: + description: The cost per app installation. + type: + - "null" + - number + cost_per_purchase: + description: The cost per purchase. + type: + - "null" + - number + cost_per_registration: + description: The cost per registration. + type: + - "null" + - number + cost_per_sales_lead: + description: The cost per sales lead. + type: + - "null" + - number + cost_per_total_sales_lead: + description: The cost per total sales lead. + type: + - "null" + - number + cost_per_total_app_event_add_to_cart: + description: The cost per total app events adding to cart. + type: + - "null" + - number + total_app_event_add_to_cart: + description: Total app events adding items to cart. + type: + - "null" + - number + dimensions: + description: + A list of dimensions for which data should be retrieved such as time, + user demographics, or content type. + type: + - "null" + - object + properties: + stat_time_day: + description: The date for which the statistical data is recorded. + type: + - "null" + - string + format: date-time + stat_time_hour: + description: The hour of the day for which the statistical data is recorded. + type: + - "null" + - string + format: date-time + campaign_id: + description: + The unique identifier for a marketing campaign within the dimension + level. + type: + - "null" + - integer + adgroup_id: + description: The unique identifier for an ad group within the dimension level. + type: + - "null" + - integer + ad_id: + description: + The unique identifier for an advertisement within the dimension + level. + type: + - "null" + - integer + advertiser_id: + description: + The unique identifier for an advertiser within the dimension + level. + type: + - "null" + - integer + audience_report: + $schema: http://json-schema.org/draft-07/schema# + additionalProperties: true + type: object + properties: + advertiser_id: + description: Unique identifier for the advertiser + type: + - "null" + - integer + adgroup_id: + description: Unique identifier for the ad group + type: + - "null" + - integer + campaign_id: + description: Unique identifier for the campaign + type: + - "null" + - integer + ad_id: + description: Unique identifier for the ad + type: + - "null" + - integer + stat_time_day: + description: Day timestamp for the statistics + type: + - "null" + - string + format: date-time + airbyte_type: timestamp_without_timezone + stat_time_hour: + description: Hour timestamp for the statistics + type: + - "null" + - string + format: date-time + airbyte_type: timestamp_without_timezone + country_code: + description: Country code of the target audience + type: + - "null" + - string + platform: + description: Platform where the ad is displayed + type: + - "null" + - string + gender: + description: Gender of the target audience + type: + - "null" + - string + age: + description: Age group of the target audience + type: + - "null" + - string + province_id: + description: Province identifier of the target audience + type: + - "null" + - string + metrics: + description: + Defines the metrics or quantitative measurements of the audience + data such as number of views, engagement rate, share count, etc. + type: + - "null" + - object + properties: + campaign_name: + description: Name of the campaign + type: + - "null" + - string + campaign_id: + description: Campaign identifier within metrics + type: + - "null" + - integer + adgroup_name: + description: Name of the ad group + type: + - "null" + - string + placement_type: + description: Type of ad placement + type: + - "null" + - string + adgroup_id: + description: Unique identifier for the ad group within metrics + type: + - "null" + - integer + ad_name: + description: Name of the ad + type: + - "null" + - string + ad_text: + description: Text content of the ad + type: + - "null" + - string + tt_app_id: + description: TikTok app identifier + type: + - "null" + - string + tt_app_name: + description: TikTok app name + type: + - "null" + - string + mobile_app_id: + description: Mobile app identifier + type: + - "null" + - string + promotion_type: + description: Type of promotion + type: + - "null" + - string + dpa_target_audience_type: + description: Dynamic product ads target audience type + type: + - "null" + - string + spend: + description: Amount spent on the ad campaign + type: + - "null" + - string + cpc: + description: Cost per click + type: + - "null" + - string + cpm: + description: Cost per 1000 impressions + type: + - "null" + - string + impressions: + description: Number of times the ad was displayed + type: + - "null" + - string + clicks: + description: Number of clicks on the ad + type: + - "null" + - string + ctr: + description: Click-through rate + type: + - "null" + - string + reach: + description: Number of unique users who saw the ad + type: + - "null" + - string + cost_per_1000_reached: + description: Cost per 1000 impressions reached + type: + - "null" + - string + conversion: + description: Number of conversions from the ad + type: + - "null" + - string + cost_per_conversion: + description: Cost per conversion + type: + - "null" + - string + conversion_rate: + description: Rate of conversions from the ad + type: + - "null" + - string + real_time_conversion: + description: Real-time conversions from the ad + type: + - "null" + - string + real_time_cost_per_conversion: + description: Real-time cost per conversion + type: + - "null" + - string + real_time_conversion_rate: + description: Real-time conversion rate + type: + - "null" + - string + result: + description: Total results achieved + type: + - "null" + - string + cost_per_result: + description: Cost per result achieved + type: + - "null" + - string + result_rate: + description: Result rate + type: + - "null" + - string + real_time_result: + description: Real-time results achieved + type: + - "null" + - string + real_time_cost_per_result: + description: Real-time cost per result achieved + type: + - "null" + - string + real_time_result_rate: + description: Real-time result rate + type: + - "null" + - string + province_id: + description: Province identifier + type: + - "null" + - string + dimensions: + description: + Specifies the dimensions or attributes of the audience data being + reported such as age, gender, location, etc. + type: + - "null" + - object + properties: + stat_time_day: + description: Day timestamp for the statistics + type: + - "null" + - string + format: date-time + stat_time_hour: + description: Hour timestamp for the statistics + type: + - "null" + - string + format: date-time + country_code: + description: Country code within dimensions + type: + - "null" + - string + campaign_id: + description: Campaign identifier within dimensions + type: + - "null" + - integer + adgroup_id: + description: Unique identifier for the ad group within dimensions + type: + - "null" + - integer + ad_id: + description: Unique identifier for the ad within dimensions + type: + - "null" + - integer + advertiser_id: + description: Unique identifier for the advertiser within dimensions + type: + - "null" + - integer + gender: + description: Gender of the target audience within dimensions + type: + - "null" + - string + age: + description: Age group within dimensions + type: + - "null" + - string + ac: + description: AC description + type: + - "null" + - string + language: + description: Language of the target audience + type: + - "null" + - string + platform: + description: Platform type of the ad + type: + - "null" + - string + interest_category: + description: Interest category of the target audience + type: + - "null" + - string + placement: + description: Placement type of the ad + type: + - "null" + - string + +streams: + - $ref: "#/definitions/advertiser_ids_stream" + - $ref: "#/definitions/advertisers_stream" + - $ref: "#/definitions/audiences_stream" + - $ref: "#/definitions/creative_assets_music_stream" + - $ref: "#/definitions/creative_assets_portfolios_stream" + - $ref: "#/definitions/campaigns_stream" + - $ref: "#/definitions/ad_groups_stream" + - $ref: "#/definitions/ads_stream" + - $ref: "#/definitions/creative_assets_images_stream" + - $ref: "#/definitions/creative_assets_videos_stream" + - $ref: "#/definitions/ads_reports_daily_stream" + - $ref: "#/definitions/ad_groups_reports_daily_stream" + - $ref: "#/definitions/advertisers_reports_daily_stream" + - $ref: "#/definitions/campaigns_reports_daily_stream" + - $ref: "#/definitions/campaigns_audience_reports_daily_stream" + - $ref: "#/definitions/ad_group_audience_reports_daily_stream" + - $ref: "#/definitions/ads_audience_reports_daily_stream" + - $ref: "#/definitions/advertisers_audience_reports_daily_stream" + - $ref: "#/definitions/campaigns_audience_reports_by_country_daily_stream" + - $ref: "#/definitions/ad_group_audience_reports_by_country_daily_stream" + - $ref: "#/definitions/ads_audience_reports_by_country_daily_stream" + - $ref: "#/definitions/advertisers_audience_reports_by_country_daily_stream" + - $ref: "#/definitions/campaigns_audience_reports_by_platform_daily_stream" + - $ref: "#/definitions/ad_group_audience_reports_by_platform_daily_stream" + - $ref: "#/definitions/ads_audience_reports_by_platform_daily_stream" + - $ref: "#/definitions/advertisers_audience_reports_by_platform_daily_stream" + - $ref: "#/definitions/ads_audience_reports_by_province_daily_stream" + - $ref: "#/definitions/ads_reports_hourly_stream" + - $ref: "#/definitions/advertisers_reports_hourly_stream" + - $ref: "#/definitions/campaigns_reports_hourly_stream" + - $ref: "#/definitions/ad_groups_reports_hourly_stream" + - $ref: "#/definitions/ads_reports_lifetime_stream" + - $ref: "#/definitions/advertisers_reports_lifetime_stream" + - $ref: "#/definitions/campaigns_reports_lifetime_stream" + - $ref: "#/definitions/ad_groups_reports_lifetime_stream" + - $ref: "#/definitions/advertisers_audience_reports_lifetime_stream" + +spec: + type: Spec + documentation_url: https://docs.airbyte.com/integrations/sources/tiktok-marketing + connection_specification: + $schema: http://json-schema.org/draft-07/schema# + title: TikTok Marketing Source Spec + additionalProperties: true + type: object + properties: + credentials: + title: Authentication Method + description: Authentication method + default: {} + order: 0 + type: object + oneOf: + - title: OAuth2.0 + type: object + properties: + auth_type: + title: Auth Type + const: oauth2.0 + order: 0 + type: string + app_id: + title: App ID + description: The Developer Application App ID. + airbyte_secret: true + type: string + secret: + title: Secret + description: The Developer Application Secret. + airbyte_secret: true + type: string + access_token: + title: Access Token + description: Long-term Authorized Access Token. + airbyte_secret: true + type: string + advertiser_id: + title: Advertiser ID + description: + The Advertiser ID to filter reports and streams. Let this + empty to retrieve all. + type: string + required: + - app_id + - secret + - access_token + - title: Sandbox Access Token + type: object + properties: + auth_type: + title: Auth Type + const: sandbox_access_token + order: 0 + type: string + advertiser_id: + title: Advertiser ID + description: + The Advertiser ID which generated for the developer's Sandbox + application. + type: string + access_token: + title: Access Token + description: The long-term authorized access token. + airbyte_secret: true + type: string + required: + - advertiser_id + - access_token + start_date: + title: Replication Start Date + description: + "The Start Date in format: YYYY-MM-DD. Any data before this date + will not be replicated. If this parameter is not set, all data will be replicated." + default: "2016-09-01" + pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + order: 1 + type: string + format: date + end_date: + title: End Date + description: + The date until which you'd like to replicate data for all incremental + streams, in the format YYYY-MM-DD. All data generated between start_date and + this date will be replicated. Not setting this option will result in always + syncing the data till the current date. + pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + order: 2 + type: string + format: date + attribution_window: + title: Attribution Window + description: The attribution window in days. + minimum: 0 + maximum: 364 + default: 3 + order: 3 + type: integer + include_deleted: + title: Include Deleted Data in Reports and Ads, Ad Groups and Campaign streams. + description: Set to active if you want to include deleted data in report based streams and Ads, Ad Groups and Campaign streams. + default: false + order: 4 + type: boolean + advanced_auth: + auth_flow_type: oauth2.0 + predicate_key: + - credentials + - auth_type + predicate_value: oauth2.0 + oauth_config_specification: + complete_oauth_output_specification: + title: CompleteOauthOutputSpecification + type: object + properties: + access_token: + title: Access Token + path_in_connector_config: + - credentials + - access_token + type: string + required: + - access_token + complete_oauth_server_input_specification: + title: CompleteOauthServerInputSpecification + type: object + properties: + app_id: + title: App Id + type: string + secret: + title: Secret + type: string + required: + - app_id + - secret + complete_oauth_server_output_specification: + title: CompleteOauthServerOutputSpecification + type: object + properties: + app_id: + title: App Id + path_in_connector_config: + - credentials + - app_id + type: string + secret: + title: Secret + path_in_connector_config: + - credentials + - secret + type: string + required: + - app_id + - secret + +metadata: + autoImportSchema: + advertiser_ids: true diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/ad_groups.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/ad_groups.json deleted file mode 100644 index 30b1ca85675f..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/ad_groups.json +++ /dev/null @@ -1,584 +0,0 @@ -{ - "type": "object", - "properties": { - "adgroup_id": { - "description": "The unique identifier of the ad group", - "type": "integer" - }, - "campaign_id": { - "description": "The unique identifier of the campaign", - "type": "integer" - }, - "advertiser_id": { - "description": "The unique identifier of the advertiser", - "type": "integer" - }, - "adgroup_name": { - "description": "The name of the ad group", - "type": "string" - }, - "placement_type": { - "description": "The type of ad placement", - "type": "string", - "enum": ["PLACEMENT_TYPE_AUTOMATIC", "PLACEMENT_TYPE_NORMAL"] - }, - "placements": { - "description": "Information about the ad placements targeted", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "inventory_filter_enabled": { - "description": "Flag indicating if inventory filter is enabled", - "type": ["null", "boolean"] - }, - "comment_disabled": { - "description": "Flag indicating if comments are disabled", - "type": "boolean" - }, - "app_id": { - "description": "The unique identifier of the app", - "type": ["null", "integer"] - }, - "promotion_type": { - "description": "The type of promotion", - "type": "string", - "enum": [ - "APP_ANDROID", - "APP_IOS", - "WEBSITE", - "LEAD_GENERATION", - "WEBSITE_OR_DISPLAY", - "TIKTOK_SHOP", - "VIDEO_SHOPPING", - "LIVE_SHOPPING" - ] - }, - "app_download_url": { - "description": "The URL for downloading the associated app", - "type": ["null", "string"] - }, - "package": { - "description": "The package used for the ad group", - "type": ["null", "string"] - }, - "pixel_id": { - "description": "The ID of the pixel used for tracking", - "type": ["null", "integer"] - }, - "optimization_event": { - "description": "The event used for optimization", - "type": ["null", "string"] - }, - "secondary_optimization_event": { - "description": "Additional event used for optimization", - "type": ["null", "string"] - }, - "creative_material_mode": { - "description": "The mode for creative materials", - "type": "string" - }, - "modify_time": { - "description": "The timestamp for when the ad group was last modified", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "create_time": { - "description": "The timestamp for when the ad group was created", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "audience_ids": { - "description": "The IDs of the targeted audience", - "type": "array", - "items": { - "type": "integer" - } - }, - "excluded_audience_ids": { - "description": "The IDs of excluded audiences", - "type": "array", - "items": { - "type": "integer" - } - }, - "audience_type": { - "description": "The type of audience being targeted", - "type": ["null", "string"] - }, - "location_ids": { - "description": "The IDs of targeted locations", - "type": "array", - "items": { - "type": "integer" - } - }, - "is_hfss": { - "description": "Flag indicating if high-frequency short sequences are included", - "type": "boolean" - }, - "interest_category_ids": { - "description": "The IDs of interest categories for targeting", - "type": "array", - "items": { - "type": "integer" - } - }, - "interest_keyword_ids": { - "description": "The IDs of interest keywords for targeting", - "type": "array", - "items": { - "type": "integer" - } - }, - "age_groups": { - "description": "The targeted age groups for the ad group", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "gender": { - "description": "The targeted gender for the ad group", - "type": ["null", "string"] - }, - "languages": { - "description": "The targeted languages for the ad group", - "type": "array", - "items": { - "type": "string" - } - }, - "operating_systems": { - "description": "The targeted operating systems", - "type": "array", - "items": { - "type": "string" - } - }, - "network_types": { - "description": "The types of networks targeted", - "type": "array", - "items": { - "type": "string" - } - }, - "device_price_ranges": { - "description": "The price ranges for devices", - "type": ["null", "array"], - "items": { - "type": "number" - } - }, - "min_android_version": { - "description": "The minimum required Android version", - "type": ["null", "string"] - }, - "ios14_targeting": { - "description": "Information about iOS 14 targeting settings", - "type": ["null", "string"] - }, - "device_model_ids": { - "description": "The IDs of targeted device models", - "type": ["null", "array"], - "items": { - "type": "integer" - } - }, - "min_ios_version": { - "description": "The minimum required iOS version", - "type": ["null", "string"] - }, - "budget_mode": { - "description": "The mode for managing the budget", - "type": "string" - }, - "budget": { - "description": "The allocated budget for the ad group", - "type": "number" - }, - "schedule_type": { - "description": "The type of scheduling", - "type": "string" - }, - "schedule_start_time": { - "description": "The start time of the scheduling", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "schedule_end_time": { - "description": "The end time of the scheduling", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "dayparting": { - "description": "Information about dayparting settings", - "type": ["null", "string"] - }, - "optimization_goal": { - "description": "The goal set for optimization", - "type": "string" - }, - "cpv_video_duration": { - "description": "The duration for cost-per-view video", - "type": ["null", "string"] - }, - "conversion_window": { - "description": "The window for tracking conversions", - "type": ["null", "string"] - }, - "pacing": { - "description": "Information about the pacing settings", - "type": ["null", "string"] - }, - "billing_event": { - "description": "The event used for billing", - "type": ["null", "string"] - }, - "skip_learning_phase": { - "description": "Flag indicating if the learning phase is skipped", - "type": "integer" - }, - "bid_type": { - "description": "The type of bidding", - "type": ["null", "string"] - }, - "bid_price": { - "description": "The price set for bidding", - "type": "number" - }, - "conversion_bid_price": { - "description": "The bid price for conversions", - "type": "number" - }, - "deep_bid_type": { - "description": "The type of deep bid strategy", - "type": ["null", "string"] - }, - "deep_cpa_bid": { - "description": "The bid amount for deep cost-per-action", - "type": "number" - }, - "secondary_status": { - "description": "The secondary status of the ad group", - "type": "string" - }, - "operation_status": { - "description": "The status of the operation", - "type": "string" - }, - "frequency": { - "description": "The frequency of ad display", - "type": ["null", "integer"] - }, - "frequency_schedule": { - "description": "The schedule for frequency capping", - "type": ["null", "integer"] - }, - "statistic_type": { - "description": "The type of statistics being tracked", - "type": ["null", "string"] - }, - "carrier_ids": { - "description": "The IDs of the targeted carriers", - "type": ["null", "array"], - "items": { - "type": "integer" - } - }, - "carriers": { - "description": "Information about the targeted carriers", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "video_download_disabled": { - "description": "Flag indicating if video downloads are disabled", - "type": "boolean" - }, - "blocked_pangle_app_ids": { - "description": "The IDs of the blocked Pangle apps", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "action_category_ids": { - "description": "The IDs of the action categories associated with the ad group", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "action_days": { - "description": "The number of days the action has been performed", - "type": ["null", "integer"] - }, - "video_actions": { - "description": "Information about video-specific actions", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "rf_purchased_type": { - "description": "Type of purchased results", - "type": ["null", "string"] - }, - "purchased_impression": { - "description": "Information about purchased impressions", - "type": ["null", "number"] - }, - "purchased_reach": { - "description": "Information about purchased reach", - "type": ["null", "number"] - }, - "rf_estimated_cpr": { - "description": "Estimated cost per result", - "type": ["null", "number"] - }, - "rf_estimated_frequency": { - "description": "Estimated frequency of results", - "type": ["null", "number"] - }, - "included_pangle_audience_package_ids": { - "description": "The IDs of included Pangle audience packages", - "type": ["null", "array"], - "items": { - "type": "number" - } - }, - "excluded_pangle_audience_package_ids": { - "description": "The IDs of excluded Pangle audience packages", - "type": ["null", "array"], - "items": { - "type": "number" - } - }, - "is_new_structure": { - "description": "Flag indicating if the ad group follows a new structure", - "type": "boolean" - }, - "is_smart_performance_campaign": { - "description": "Flag indicating if the campaign is using smart performance", - "type": ["null", "boolean"] - }, - "catalog_id": { - "description": "The unique identifier of the catalog", - "type": ["null", "integer"] - }, - "product_set_id": { - "description": "The ID of the product set", - "type": ["null", "integer"] - }, - "catalog_authorized_bc_id": { - "description": "The authorized Business Center ID for the catalog", - "type": ["null", "integer"] - }, - "audience_rule": { - "description": "The rule set for targeting the audience", - "type": ["null", "object"] - }, - "included_custom_actions": { - "description": "Custom actions that are included", - "type": ["null", "array"], - "items": { - "type": "object" - } - }, - "excluded_custom_actions": { - "description": "Custom actions that are excluded", - "type": ["null", "array"], - "items": { - "type": "object" - } - }, - "shopping_ads_retargeting_type": { - "description": "The type of retargeting used for shopping ads", - "type": ["null", "string"] - }, - "split_test_adgroup_ids": { - "description": "The IDs of ad groups participating in split testing", - "type": ["null", "array"], - "items": { - "type": "number" - } - }, - "brand_safety_type": { - "description": "The type of brand safety measures", - "type": ["null", "string"] - }, - "brand_safety_partner": { - "description": "Information about the brand safety partners", - "type": ["null", "string"] - }, - "promotion_website_type": { - "description": "The type of website used for promotion", - "type": ["null", "string"] - }, - "ios_quota_type": { - "description": "The type of iOS quota", - "type": ["null", "string"] - }, - "roas_bid": { - "description": "The bid amount set for return on ad spend", - "type": ["null", "number"] - }, - "actions": { - "description": "Information about the actions taken on the ad group", - "type": ["null", "array"], - "items": { - "type": ["null", "object"], - "properties": { - "action_category_ids": { - "description": "The IDs of the action categories for the specific action", - "type": ["null", "array"], - "items": { - "type": "integer" - } - }, - "action_period": { - "description": "The period during which the action was taken", - "type": ["null", "number"] - }, - "action_scene": { - "description": "The scene in which the action took place", - "type": ["null", "string"] - }, - "video_user_actions": { - "description": "User actions specific to video content", - "type": ["null", "array"], - "items": { - "type": "string" - } - } - } - } - }, - "targeting_expansion": { - "description": "Settings for targeting expansion", - "type": ["null", "object"], - "properties": { - "expansion_enabled": { - "description": "Flag indicating if targeting expansion is enabled", - "type": "boolean" - }, - "expansion_types": { - "description": "Types of expansion enabled", - "type": ["null", "array"], - "items": { - "type": "string" - } - } - } - }, - "schedule_infos": { - "description": "Information about the scheduling arrangements", - "type": ["null", "array"], - "items": { - "type": "object" - } - }, - "share_disabled": { - "description": "Flag indicating if sharing is disabled", - "type": ["null", "boolean"] - }, - "auto_targeting_enabled": { - "description": "Flag indicating if auto-targeting is enabled", - "type": ["null", "boolean"] - }, - "ios14_quota_type": { - "description": "The type of iOS 14 quota", - "type": ["null", "string"] - }, - "campaign_name": { - "description": "The name of the campaign", - "type": ["null", "string"] - }, - "bid_display_mode": { - "description": "The display mode for bidding", - "type": ["null", "string"] - }, - "scheduled_budget": { - "description": "The budget allocated for scheduling", - "type": ["null", "number"] - }, - "adgroup_app_profile_page_state": { - "description": "The state of the app profile page related to the ad group", - "type": ["null", "string"] - }, - "keywords": { - "description": "Keywords associated with the ad group", - "type": ["null", "string"] - }, - "next_day_retention": { - "description": "Retention information for the next day", - "type": ["null", "number"] - }, - "category_id": { - "description": "The ID of the category for the ad group", - "type": ["null", "integer"] - }, - "search_result_enabled": { - "description": "Flag indicating if search results are enabled", - "type": ["null", "boolean"] - }, - "app_type": { - "description": "The type of the associated app", - "type": ["null", "string"] - }, - "feed_type": { - "description": "The type of feed used", - "type": ["null", "string"] - }, - "delivery_mode": { - "description": "The mode for delivery", - "type": ["null", "string"] - }, - "category_exclusion_ids": { - "description": "The IDs of the excluded categories", - "type": ["null", "array"], - "items": { - "type": "string" - } - }, - "contextual_tag_ids": { - "description": "The IDs of contextual tags for targeting", - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - }, - "household_income": { - "description": "The targeted household income groups", - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - }, - "isp_ids": { - "description": "The IDs of the targeted internet service providers", - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - }, - "spending_power": { - "description": "Information about the spending power targeted", - "type": ["null", "string"] - }, - "zipcode_ids": { - "description": "The IDs of targeted ZIP codes", - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/ads.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/ads.json deleted file mode 100644 index 2fa88b862bf7..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/ads.json +++ /dev/null @@ -1,393 +0,0 @@ -{ - "type": "object", - "properties": { - "advertiser_id": { - "description": "The unique identifier of the advertiser", - "type": "integer" - }, - "campaign_id": { - "description": "The unique identifier of the campaign", - "type": "integer" - }, - "campaign_name": { - "description": "The name of the campaign", - "type": "string" - }, - "adgroup_id": { - "description": "The unique identifier of the ad group", - "type": "integer" - }, - "adgroup_name": { - "description": "The name of the ad group", - "type": "string" - }, - "ad_id": { - "description": "The unique identifier of the ad", - "type": "integer" - }, - "ad_name": { - "description": "The name of the ad", - "type": "string" - }, - "tracking_app_id": { - "description": "The unique identifier of the tracking app", - "type": ["null", "string"] - }, - "tracking_offline_event_set_ids": { - "description": "The unique identifiers of offline event sets for tracking", - "type": ["null", "array"], - "items": { - "description": "Unique identifier of an offline event set", - "type": ["null", "string"] - } - }, - "call_to_action": { - "description": "The call-to-action text for the ad", - "type": ["null", "string"] - }, - "call_to_action_id": { - "description": "The identifier of the call-to-action", - "type": ["null", "string"] - }, - "disclaimer_type": { - "description": "The type of disclaimer displayed", - "type": ["null", "string"] - }, - "disclaimer_text": { - "description": "The disclaimer text", - "type": ["null", "object"], - "properties": { - "text": { - "description": "The text of the disclaimer", - "type": ["null", "string"] - } - } - }, - "disclaimer_clickable_texts": { - "description": "Clickable disclaimer texts with URLs", - "type": ["null", "object"], - "properties": { - "text": { - "description": "The disclaimer text", - "type": ["null", "string"] - }, - "url": { - "description": "The URL associated with the disclaimer text", - "type": ["null", "string"] - } - } - }, - "card_id": { - "description": "The identifier of the card", - "type": ["null", "integer"] - }, - "secondary_status": { - "description": "The secondary status of the ad", - "type": "string" - }, - "operation_status": { - "description": "The operational status of the ad", - "type": ["null", "string"] - }, - "is_aco": { - "description": "Indicates if the ad is under Automated Creative Optimization", - "type": ["null", "boolean"] - }, - "image_ids": { - "description": "The unique identifiers of images used in the ad", - "type": ["null", "array"], - "items": { - "description": "Unique identifier of an image", - "type": "string" - } - }, - "image_mode": { - "description": "The mode of displaying images", - "type": ["null", "string"] - }, - "ad_format": { - "description": "The format of the ad (e.g., image, video, carousel)", - "type": ["null", "string"] - }, - "ad_text": { - "description": "The text content of the ad", - "type": ["null", "string"] - }, - "ad_texts": { - "description": "The text content of the ad in various languages", - "type": ["null", "array"], - "items": { - "description": "Text content in a specific language", - "type": "string" - } - }, - "video_id": { - "description": "The unique identifier of the video", - "type": ["null", "string"] - }, - "tiktok_item_id": { - "description": "The unique identifier of the TikTok item", - "type": ["null", "string"] - }, - "premium_badge_id": { - "description": "The unique identifier of the premium badge", - "type": ["null", "string"] - }, - "app_name": { - "description": "The name of the mobile app where the ad is displayed", - "type": ["null", "string"] - }, - "landing_page_url": { - "description": "The URL of the landing page for the ad", - "type": ["null", "string"] - }, - "landing_page_urls": { - "description": "The URLs of landing pages for the ad", - "type": ["null", "array"], - "items": { - "description": "URL of a landing page", - "type": "string" - } - }, - "display_name": { - "description": "The display name of the ad", - "type": ["null", "string"] - }, - "profile_image_url": { - "description": "The URL of the profile image associated with the ad", - "type": ["null", "string"] - }, - "impression_tracking_url": { - "description": "The URL for tracking ad impressions", - "type": ["null", "string"] - }, - "click_tracking_url": { - "description": "The URL for tracking ad clicks", - "type": ["null", "string"] - }, - "tracking_pixel_id": { - "description": "The unique identifier of the tracking pixel", - "type": ["null", "integer"] - }, - "deeplink": { - "description": "The deeplink URL for the ad", - "type": ["null", "string"] - }, - "deeplink_type": { - "description": "The type of deeplink used", - "type": ["null", "string"] - }, - "fallback_type": { - "description": "The type of fallback used", - "type": ["null", "string"] - }, - "playable_url": { - "description": "The URL for a playable ad", - "type": ["null", "string"] - }, - "vast_moat_enabled": { - "description": "Indicates if VAST MOAT is enabled", - "type": ["null", "boolean"] - }, - "page_id": { - "description": "The unique identifier of the page", - "type": ["null", "number"] - }, - "creative_authorized": { - "description": "Indicates if the creative is authorized", - "type": ["null", "boolean"] - }, - "is_new_structure": { - "description": "Indicates if the ad is part of a new structure", - "type": ["null", "boolean"] - }, - "create_time": { - "description": "The timestamp when the ad was created", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "modify_time": { - "description": "The timestamp when the ad was last modified", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "shopping_ads_fallback_type": { - "description": "The type of fallback for shopping ads", - "type": ["null", "string"] - }, - "shopping_deeplink_type": { - "description": "The type of deeplink for shopping", - "type": ["null", "string"] - }, - "shopping_ads_video_package_id": { - "description": "The unique identifier of the video package for shopping ads", - "type": ["null", "string"] - }, - "promotional_music_disabled": { - "description": "Indicates if promotional music is disabled", - "type": ["null", "boolean"] - }, - "item_duet_status": { - "description": "The status of item duet", - "type": ["null", "string"] - }, - "item_stitch_status": { - "description": "The status of item stitch", - "type": ["null", "string"] - }, - "avatar_icon_web_uri": { - "description": "The URL of the avatar icon for the ad", - "type": ["null", "string"] - }, - "brand_safety_postbid_partner": { - "description": "Details about post-bidding partner for brand safety", - "type": ["null", "string"] - }, - "brand_safety_vast_url": { - "description": "The VAST URL for brand safety tracking", - "type": ["null", "string"] - }, - "creative_type": { - "description": "The type of creative used in the ad", - "type": ["null", "string"] - }, - "identity_id": { - "description": "The identifier of the identity", - "type": ["null", "string"] - }, - "identity_type": { - "description": "The type of identity", - "type": ["null", "string"] - }, - "identity_authorized_bc_id": { - "description": "The authorized identity for branded content", - "type": ["null", "string"] - }, - "phone_region_code": { - "description": "The region code for the phone number", - "type": ["null", "string"] - }, - "phone_region_calling_code": { - "description": "The calling code region for the phone number", - "type": ["null", "string"] - }, - "optimization_event": { - "description": "The event used for optimization", - "type": ["null", "string"] - }, - "phone_number": { - "description": "The phone number associated with the ad", - "type": ["null", "string"] - }, - "carousel_image_index": { - "description": "The index of the image in a carousel ad", - "type": ["null", "integer"] - }, - "viewability_postbid_partner": { - "description": "Details about post-bidding partner for viewability tracking", - "type": ["null", "string"] - }, - "viewability_vast_url": { - "description": "The VAST URL for viewability tracking", - "type": ["null", "string"] - }, - "music_id": { - "description": "The unique identifier of the music used in the ad", - "type": ["null", "string"] - }, - "utm_params": { - "description": "UTM parameters for tracking", - "type": ["null", "array"], - "items": { - "description": "Key-value pair for a UTM parameter", - "type": ["null", "object"], - "properties": { - "key": { - "description": "The key of the UTM parameter", - "type": ["null", "string"] - }, - "value": { - "description": "The value of the UTM parameter", - "type": ["null", "string"] - } - } - } - }, - "shopping_ads_deeplink_type": { - "description": "The type of deeplink for shopping ads", - "type": ["null", "string"] - }, - "dark_post_status": { - "description": "The status of dark post", - "type": ["null", "string"] - }, - "branded_content_disabled": { - "description": "Indicates if branded content is disabled", - "type": ["null", "string"] - }, - "product_specific_type": { - "description": "The specific type of product", - "type": ["null", "string"] - }, - "catalog_id": { - "description": "The unique identifier of the catalog", - "type": ["null", "string"] - }, - "item_group_ids": { - "description": "The unique identifiers of item groups", - "type": ["null", "array"], - "items": { - "description": "Unique identifier of an item group", - "type": ["null", "string"] - } - }, - "product_set_id": { - "description": "The unique identifier of the product set", - "type": ["null", "string"] - }, - "sku_ids": { - "description": "The unique identifiers of SKUs associated with the ad", - "type": ["null", "array"], - "items": { - "description": "Unique identifier of a SKU", - "type": ["null", "string"] - } - }, - "dynamic_format": { - "description": "The dynamic format of the ad", - "type": ["null", "string"] - }, - "vertical_video_strategy": { - "description": "The strategy for displaying vertical videos", - "type": ["null", "string"] - }, - "dynamic_destination": { - "description": "The dynamic destination of the ad", - "type": ["null", "string"] - }, - "showcase_products": { - "description": "Products displayed in a showcase ad", - "type": ["null", "object"], - "properties": { - "item_group_id": { - "description": "The unique identifier of the item group", - "type": ["null", "string"] - }, - "store_id": { - "description": "The unique identifier of the store", - "type": ["null", "string"] - }, - "catalog_id": { - "description": "The unique identifier of the catalog", - "type": ["null", "string"] - } - } - }, - "tiktok_page_category": { - "description": "The category of the TikTok page", - "type": ["null", "string"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/advertiser_ids.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/advertiser_ids.json deleted file mode 100644 index 58c094caba7d..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/advertiser_ids.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "object", - "properties": { - "advertiser_id": { - "description": "The unique identifier for each advertiser in the TikTok marketing platform.", - "type": "integer" - }, - "advertiser_name": { - "description": "The name of the advertiser registered in the TikTok marketing platform.", - "type": "string" - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/advertisers.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/advertisers.json deleted file mode 100644 index 11031d5d61ca..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/advertisers.json +++ /dev/null @@ -1,121 +0,0 @@ -{ - "type": "object", - "properties": { - "advertiser_id": { - "description": "Unique identifier for the advertiser.", - "type": "integer" - }, - "name": { - "description": "The name of the advertiser or company.", - "type": "string" - }, - "address": { - "description": "The physical address of the advertiser.", - "type": ["null", "string"] - }, - "company": { - "description": "The name of the company associated with the advertiser.", - "type": ["null", "string"] - }, - "contacter": { - "description": "The contact person for the advertiser.", - "type": ["null", "string"] - }, - "country": { - "description": "The country where the advertiser is located.", - "type": ["null", "string"] - }, - "currency": { - "description": "The currency used for transactions in the account.", - "type": ["null", "string"] - }, - "description": { - "description": "A brief description or bio of the advertiser or company.", - "type": ["null", "string"] - }, - "email": { - "description": "The email address associated with the advertiser.", - "type": ["null", "string"] - }, - "industry": { - "description": "The industry or sector the advertiser operates in.", - "type": ["null", "string"] - }, - "language": { - "description": "The preferred language of communication for the advertiser.", - "type": ["null", "string"] - }, - "license_no": { - "description": "The license number of the advertiser.", - "type": ["null", "string"] - }, - "license_url": { - "description": "The URL link to the advertiser's license documentation.", - "type": ["null", "string"] - }, - "cellphone_number": { - "description": "The cellphone number of the advertiser.", - "type": ["null", "string"] - }, - "promotion_area": { - "description": "The specific area or region where the advertiser focuses promotion.", - "type": ["null", "string"] - }, - "rejection_reason": { - "description": "Reason for any advertisement rejection by the platform.", - "type": ["null", "string"] - }, - "role": { - "description": "The role or position of the advertiser within the company.", - "type": ["null", "string"] - }, - "status": { - "description": "The current status of the advertiser's account.", - "type": ["null", "string"] - }, - "timezone": { - "description": "The timezone setting for the advertiser's activities.", - "type": ["null", "string"] - }, - "balance": { - "description": "The current balance in the advertiser's account.", - "type": "number" - }, - "create_time": { - "description": "The timestamp when the advertiser account was created.", - "type": "integer" - }, - "telephone_number": { - "description": "The telephone number of the advertiser.", - "type": ["null", "string"] - }, - "display_timezone": { - "description": "The timezone for display purposes.", - "type": ["null", "string"] - }, - "promotion_center_province": { - "description": "The province or state at the center of the advertiser's promotion activities.", - "type": ["null", "string"] - }, - "advertiser_account_type": { - "description": "The type of advertiser's account (e.g., individual, business).", - "type": ["null", "string"] - }, - "license_city": { - "description": "The city where the advertiser's license is registered.", - "type": ["null", "string"] - }, - "brand": { - "description": "The brand name associated with the advertiser.", - "type": ["null", "string"] - }, - "license_province": { - "description": "The province or state where the advertiser's license is registered.", - "type": ["null", "string"] - }, - "promotion_center_city": { - "description": "The city at the center of the advertiser's promotion activities.", - "type": ["null", "string"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/audience_reports.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/audience_reports.json deleted file mode 100644 index a504f4cfe25b..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/audience_reports.json +++ /dev/null @@ -1,256 +0,0 @@ -{ - "type": "object", - "additionalProperties": true, - "properties": { - "advertiser_id": { - "description": "Unique identifier for the advertiser", - "type": ["null", "integer"] - }, - "adgroup_id": { - "description": "Unique identifier for the ad group", - "type": ["null", "integer"] - }, - "campaign_id": { - "description": "Unique identifier for the campaign", - "type": ["null", "integer"] - }, - "ad_id": { - "description": "Unique identifier for the ad", - "type": ["null", "integer"] - }, - "stat_time_day": { - "description": "Day timestamp for the statistics", - "type": ["null", "string"], - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "stat_time_hour": { - "description": "Hour timestamp for the statistics", - "type": ["null", "string"], - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "country_code": { - "description": "Country code of the target audience", - "type": ["null", "string"] - }, - "platform": { - "description": "Platform where the ad is displayed", - "type": ["null", "string"] - }, - "gender": { - "description": "Gender of the target audience", - "type": ["null", "string"] - }, - "age": { - "description": "Age group of the target audience", - "type": ["null", "string"] - }, - "province_id": { - "description": "Province identifier of the target audience", - "type": ["null", "string"] - }, - "metrics": { - "description": "Defines the metrics or quantitative measurements of the audience data such as number of views, engagement rate, share count, etc.", - "type": ["null", "object"], - "properties": { - "campaign_name": { - "description": "Name of the campaign", - "type": ["null", "string"] - }, - "campaign_id": { - "description": "Campaign identifier within metrics", - "type": ["null", "integer"] - }, - "adgroup_name": { - "description": "Name of the ad group", - "type": ["null", "string"] - }, - "placement_type": { - "description": "Type of ad placement", - "type": ["null", "string"] - }, - "adgroup_id": { - "description": "Unique identifier for the ad group within metrics", - "type": ["null", "integer"] - }, - "ad_name": { - "description": "Name of the ad", - "type": ["null", "string"] - }, - "ad_text": { - "description": "Text content of the ad", - "type": ["null", "string"] - }, - "tt_app_id": { - "description": "TikTok app identifier", - "type": ["null", "string"] - }, - "tt_app_name": { - "description": "TikTok app name", - "type": ["null", "string"] - }, - "mobile_app_id": { - "description": "Mobile app identifier", - "type": ["null", "string"] - }, - "promotion_type": { - "description": "Type of promotion", - "type": ["null", "string"] - }, - "dpa_target_audience_type": { - "description": "Dynamic product ads target audience type", - "type": ["null", "string"] - }, - "spend": { - "description": "Amount spent on the ad campaign", - "type": ["null", "string"] - }, - "cpc": { - "description": "Cost per click", - "type": ["null", "string"] - }, - "cpm": { - "description": "Cost per 1000 impressions", - "type": ["null", "string"] - }, - "impressions": { - "description": "Number of times the ad was displayed", - "type": ["null", "string"] - }, - "clicks": { - "description": "Number of clicks on the ad", - "type": ["null", "string"] - }, - "ctr": { - "description": "Click-through rate", - "type": ["null", "string"] - }, - "reach": { - "description": "Number of unique users who saw the ad", - "type": ["null", "string"] - }, - "cost_per_1000_reached": { - "description": "Cost per 1000 impressions reached", - "type": ["null", "string"] - }, - "conversion": { - "description": "Number of conversions from the ad", - "type": ["null", "string"] - }, - "cost_per_conversion": { - "description": "Cost per conversion", - "type": ["null", "string"] - }, - "conversion_rate": { - "description": "Rate of conversions from the ad", - "type": ["null", "string"] - }, - "real_time_conversion": { - "description": "Real-time conversions from the ad", - "type": ["null", "string"] - }, - "real_time_cost_per_conversion": { - "description": "Real-time cost per conversion", - "type": ["null", "string"] - }, - "real_time_conversion_rate": { - "description": "Real-time conversion rate", - "type": ["null", "string"] - }, - "result": { - "description": "Total results achieved", - "type": ["null", "string"] - }, - "cost_per_result": { - "description": "Cost per result achieved", - "type": ["null", "string"] - }, - "result_rate": { - "description": "Result rate", - "type": ["null", "string"] - }, - "real_time_result": { - "description": "Real-time results achieved", - "type": ["null", "string"] - }, - "real_time_cost_per_result": { - "description": "Real-time cost per result achieved", - "type": ["null", "string"] - }, - "real_time_result_rate": { - "description": "Real-time result rate", - "type": ["null", "string"] - }, - "province_id": { - "description": "Province identifier", - "type": ["null", "string"] - } - } - }, - "dimensions": { - "description": "Specifies the dimensions or attributes of the audience data being reported such as age, gender, location, etc.", - "type": ["null", "object"], - "properties": { - "stat_time_day": { - "description": "Day timestamp for the statistics", - "type": ["null", "string"], - "format": "date-time" - }, - "stat_time_hour": { - "description": "Hour timestamp for the statistics", - "type": ["null", "string"], - "format": "date-time" - }, - "country_code": { - "description": "Country code within dimensions", - "type": ["null", "string"] - }, - "campaign_id": { - "description": "Campaign identifier within dimensions", - "type": ["null", "integer"] - }, - "adgroup_id": { - "description": "Unique identifier for the ad group within dimensions", - "type": ["null", "integer"] - }, - "ad_id": { - "description": "Unique identifier for the ad within dimensions", - "type": ["null", "integer"] - }, - "advertiser_id": { - "description": "Unique identifier for the advertiser within dimensions", - "type": ["null", "integer"] - }, - "gender": { - "description": "Gender of the target audience within dimensions", - "type": ["null", "string"] - }, - "age": { - "description": "Age group within dimensions", - "type": ["null", "string"] - }, - "ac": { - "description": "AC description", - "type": ["null", "string"] - }, - "language": { - "description": "Language of the target audience", - "type": ["null", "string"] - }, - "platform": { - "description": "Platform type of the ad", - "type": ["null", "string"] - }, - "interest_category": { - "description": "Interest category of the target audience", - "type": ["null", "string"] - }, - "placement": { - "description": "Placement type of the ad", - "type": ["null", "string"] - } - } - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/audiences.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/audiences.json deleted file mode 100644 index 4f2bc2504ec8..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/audiences.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "type": "object", - "properties": { - "shared": { - "description": "Flag indicating if the audience is shared with others", - "type": ["null", "boolean"] - }, - "is_creator": { - "description": "Flag indicating if the audience creator is the user", - "type": ["null", "boolean"] - }, - "audience_id": { - "description": "Unique identifier for the audience", - "type": ["null", "string"] - }, - "cover_num": { - "description": "Number of audience members covered", - "type": ["null", "integer"] - }, - "create_time": { - "description": "Timestamp indicating when the audience was created", - "type": ["null", "string"], - "format": "date-time" - }, - "is_valid": { - "description": "Flag indicating if the audience data is valid", - "type": ["null", "boolean"] - }, - "is_expiring": { - "description": "Flag indicating if the audience data is expiring soon", - "type": ["null", "boolean"] - }, - "expired_time": { - "description": "Timestamp indicating when the audience data expires", - "type": ["null", "string"], - "format": "date-time" - }, - "name": { - "description": "Name of the audience", - "type": ["null", "string"] - }, - "audience_type": { - "description": "Type of audience (e.g., demographic, interest-based)", - "type": ["null", "string"] - }, - "calculate_type": { - "description": "Method used to calculate audience data", - "type": ["null", "string"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/basic_reports.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/basic_reports.json deleted file mode 100644 index 5702675a4cd4..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/basic_reports.json +++ /dev/null @@ -1,396 +0,0 @@ -{ - "type": "object", - "additionalProperties": true, - "properties": { - "stat_time_day": { - "description": "The date for which the statistical data is recorded.", - "type": ["null", "string"], - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "stat_time_hour": { - "description": "The hour of the day for which the statistical data is recorded.", - "type": ["null", "string"], - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "campaign_id": { - "description": "The unique identifier for a marketing campaign.", - "type": ["null", "integer"] - }, - "adgroup_id": { - "description": "The unique identifier for an ad group.", - "type": ["null", "integer"] - }, - "ad_id": { - "description": "The unique identifier for an advertisement.", - "type": ["null", "integer"] - }, - "advertiser_id": { - "description": "The unique identifier for an advertiser.", - "type": ["null", "integer"] - }, - "metrics": { - "description": "A list of metrics for which data should be retrieved such as views, likes, comments, or shares.", - "type": ["null", "object"], - "properties": { - "campaign_name": { - "description": "The name of the marketing campaign.", - "type": ["null", "string"] - }, - "campaign_id": { - "description": "The unique identifier for a marketing campaign within the metrics level.", - "type": ["null", "integer"] - }, - "adgroup_name": { - "description": "The name of the ad group.", - "type": ["null", "string"] - }, - "placement_type": { - "description": "Type of advertisement placement.", - "type": ["null", "string"] - }, - "adgroup_id": { - "description": "The unique identifier for an ad group within the metrics level.", - "type": ["null", "integer"] - }, - "ad_name": { - "description": "The name of the advertisement.", - "type": ["null", "string"] - }, - "ad_text": { - "description": "The content or text of the advertisement.", - "type": ["null", "string"] - }, - "tt_app_id": { - "description": "The unique identifier for a TikTok application.", - "type": ["null", "integer"] - }, - "tt_app_name": { - "description": "The name of the TikTok application.", - "type": ["null", "string"] - }, - "mobile_app_id": { - "description": "The unique identifier for a mobile application.", - "type": ["null", "string"] - }, - "promotion_type": { - "description": "Type of promotion.", - "type": ["null", "string"] - }, - "dpa_target_audience_type": { - "description": "Dynamic product ad target audience type.", - "type": ["null", "string"] - }, - "spend": { - "description": "Amount of money spent.", - "type": ["null", "string"] - }, - "cash_spend": { - "description": "The amount of money spent in cash.", - "type": ["null", "string"] - }, - "voucher_spend": { - "description": "Amount spent on vouchers.", - "type": ["null", "string"] - }, - "cpc": { - "description": "Cost per click.", - "type": ["null", "string"] - }, - "cpm": { - "description": "Cost per thousand impressions.", - "type": ["null", "string"] - }, - "impressions": { - "description": "Number of times the advertisement is viewed.", - "type": ["null", "string"] - }, - "clicks": { - "description": "The number of clicks on the advertisement.", - "type": ["null", "string"] - }, - "ctr": { - "description": "Click-through rate.", - "type": ["null", "string"] - }, - "reach": { - "description": "Total number of unique users reached.", - "type": ["null", "string"] - }, - "cost_per_1000_reached": { - "description": "The cost per 1000 reached users.", - "type": ["null", "string"] - }, - "conversion": { - "description": "The number of conversions.", - "type": ["null", "string"] - }, - "cost_per_conversion": { - "description": "The cost per conversion.", - "type": ["null", "string"] - }, - "conversion_rate": { - "description": "The rate of conversion.", - "type": ["null", "string"] - }, - "real_time_conversion": { - "description": "Real-time conversions.", - "type": ["null", "string"] - }, - "real_time_cost_per_conversion": { - "description": "Cost per conversion in real-time.", - "type": ["null", "string"] - }, - "real_time_conversion_rate": { - "description": "Real-time conversion rate.", - "type": ["null", "string"] - }, - "result": { - "description": "Number of results.", - "type": ["null", "string"] - }, - "cost_per_result": { - "description": "The cost per result.", - "type": ["null", "string"] - }, - "result_rate": { - "description": "Rate of results.", - "type": ["null", "string"] - }, - "real_time_result": { - "description": "Real-time results.", - "type": ["null", "string"] - }, - "real_time_cost_per_result": { - "description": "Cost per result in real-time.", - "type": ["null", "string"] - }, - "real_time_result_rate": { - "description": "Real-time result rate.", - "type": ["null", "string"] - }, - "secondary_goal_result": { - "description": "Results for secondary goals.", - "type": ["null", "string"] - }, - "cost_per_secondary_goal_result": { - "description": "The cost per secondary goal result.", - "type": ["null", "string"] - }, - "secondary_goal_result_rate": { - "description": "Rate of results for secondary goals.", - "type": ["null", "string"] - }, - "frequency": { - "description": "Frequency of occurrence.", - "type": ["null", "string"] - }, - "total_purchase_value": { - "description": "Total value of purchases made.", - "type": ["null", "string"] - }, - "total_onsite_shopping_value": { - "description": "Total value of onsite shopping.", - "type": ["null", "string"] - }, - "onsite_shopping": { - "description": "Shopping happening on the site.", - "type": ["null", "string"] - }, - "vta_purchase": { - "description": "Purchase through vertical takeoff ad (VTA).", - "type": ["null", "string"] - }, - "cta_purchase": { - "description": "Purchase through call-to-action.", - "type": ["null", "string"] - }, - "cta_conversion": { - "description": "Conversion through call-to-action.", - "type": ["null", "string"] - }, - "vta_conversion": { - "description": "Conversion through vertical takeoff ad (VTA).", - "type": ["null", "string"] - }, - "total_pageview": { - "description": "Total number of page views.", - "type": ["null", "string"] - }, - "complete_payment": { - "description": "The number of completed payments.", - "type": ["null", "string"] - }, - "value_per_complete_payment": { - "description": "Value per completed payment.", - "type": ["null", "string"] - }, - "total_complete_payment_rate": { - "description": "Rate of total completed payments.", - "type": ["null", "string"] - }, - "video_play_actions": { - "description": "Actions related to video plays.", - "type": ["null", "number"] - }, - "video_watched_2s": { - "description": "Number of viewers watching at least 2 seconds of the video.", - "type": ["null", "number"] - }, - "video_watched_6s": { - "description": "Number of viewers watching at least 6 seconds of the video.", - "type": ["null", "number"] - }, - "average_video_play": { - "description": "The average number of video plays.", - "type": ["null", "number"] - }, - "average_video_play_per_user": { - "description": "The average number of video plays per user.", - "type": ["null", "number"] - }, - "video_views_p25": { - "description": "Percentage of viewers watching at least 25% of the video.", - "type": ["null", "number"] - }, - "video_views_p50": { - "description": "Percentage of viewers watching at least 50% of the video.", - "type": ["null", "number"] - }, - "video_views_p75": { - "description": "Percentage of viewers watching at least 75% of the video.", - "type": ["null", "number"] - }, - "video_views_p100": { - "description": "Percentage of viewers watching the entire video.", - "type": ["null", "number"] - }, - "profile_visits": { - "description": "Number of visits to the profile.", - "type": ["null", "number"] - }, - "likes": { - "description": "Number of likes received.", - "type": ["null", "number"] - }, - "comments": { - "description": "The number of comments received.", - "type": ["null", "number"] - }, - "shares": { - "description": "Number of shares.", - "type": ["null", "number"] - }, - "follows": { - "description": "Number of follows.", - "type": ["null", "number"] - }, - "clicks_on_music_disc": { - "description": "The number of clicks on the music disc.", - "type": ["null", "number"] - }, - "real_time_app_install": { - "description": "Real-time app installations.", - "type": ["null", "number"] - }, - "real_time_app_install_cost": { - "description": "Cost of real-time app installations.", - "type": ["null", "number"] - }, - "app_install": { - "description": "The number of app installations.", - "type": ["null", "number"] - }, - "profile_visits_rate": { - "description": "Rate of profile visits.", - "type": ["null", "number"] - }, - "purchase": { - "description": "Number of purchases made.", - "type": ["null", "number"] - }, - "purchase_rate": { - "description": "Rate of purchases.", - "type": ["null", "number"] - }, - "registration": { - "description": "Number of registrations.", - "type": ["null", "number"] - }, - "registration_rate": { - "description": "Rate of registrations.", - "type": ["null", "number"] - }, - "sales_lead": { - "description": "Number of sales leads.", - "type": ["null", "number"] - }, - "sales_lead_rate": { - "description": "Rate of sales leads.", - "type": ["null", "number"] - }, - "cost_per_app_install": { - "description": "The cost per app installation.", - "type": ["null", "number"] - }, - "cost_per_purchase": { - "description": "The cost per purchase.", - "type": ["null", "number"] - }, - "cost_per_registration": { - "description": "The cost per registration.", - "type": ["null", "number"] - }, - "cost_per_sales_lead": { - "description": "The cost per sales lead.", - "type": ["null", "number"] - }, - "cost_per_total_sales_lead": { - "description": "The cost per total sales lead.", - "type": ["null", "number"] - }, - "cost_per_total_app_event_add_to_cart": { - "description": "The cost per total app events adding to cart.", - "type": ["null", "number"] - }, - "total_app_event_add_to_cart": { - "description": "Total app events adding items to cart.", - "type": ["null", "number"] - } - } - }, - "dimensions": { - "description": "A list of dimensions for which data should be retrieved such as time, user demographics, or content type.", - "type": ["null", "object"], - "properties": { - "stat_time_day": { - "description": "The date for which the statistical data is recorded.", - "type": ["null", "string"], - "format": "date-time" - }, - "stat_time_hour": { - "description": "The hour of the day for which the statistical data is recorded.", - "type": ["null", "string"], - "format": "date-time" - }, - "campaign_id": { - "description": "The unique identifier for a marketing campaign within the dimension level.", - "type": ["null", "integer"] - }, - "adgroup_id": { - "description": "The unique identifier for an ad group within the dimension level.", - "type": ["null", "integer"] - }, - "ad_id": { - "description": "The unique identifier for an advertisement within the dimension level.", - "type": ["null", "integer"] - }, - "advertiser_id": { - "description": "The unique identifier for an advertiser within the dimension level.", - "type": ["null", "integer"] - } - } - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/campaigns.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/campaigns.json deleted file mode 100644 index 38db717bb1e7..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/campaigns.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "type": "object", - "properties": { - "campaign_id": { - "description": "The unique identifier of the campaign", - "type": "integer" - }, - "campaign_name": { - "description": "Name of the campaign for easy identification", - "type": "string" - }, - "campaign_type": { - "description": "Type of campaign (e.g., awareness, conversion)", - "type": "string" - }, - "advertiser_id": { - "description": "The unique identifier of the advertiser associated with the campaign", - "type": "integer" - }, - "budget": { - "description": "Total budget allocated for the campaign", - "type": "number" - }, - "budget_mode": { - "description": "Mode in which the budget is being managed (e.g., daily, lifetime)", - "type": "string" - }, - "secondary_status": { - "description": "Additional status information of the campaign", - "type": "string" - }, - "operation_status": { - "description": "Current operational status of the campaign (e.g., active, paused)", - "type": ["null", "string"] - }, - "objective": { - "description": "The objective or goal of the campaign", - "type": ["null", "string"] - }, - "objective_type": { - "description": "Type of objective selected for the campaign (e.g., brand awareness, app installs)", - "type": ["null", "string"] - }, - "budget_optimize_on": { - "description": "The metric or event that the budget optimization is based on", - "type": ["null", "boolean"] - }, - "bid_type": { - "description": "Type of bid strategy being used in the campaign", - "type": ["null", "string"] - }, - "deep_bid_type": { - "description": "Advanced bid type used for campaign optimization", - "type": ["null", "string"] - }, - "optimization_goal": { - "description": "Specific goal to be optimized for in the campaign", - "type": ["null", "string"] - }, - "split_test_variable": { - "description": "Variable being tested in a split test campaign", - "type": ["null", "string"] - }, - "is_new_structure": { - "description": "Flag indicating if the campaign utilizes a new campaign structure", - "type": "boolean" - }, - "create_time": { - "description": "Timestamp when the campaign was created", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "modify_time": { - "description": "Timestamp when the campaign was last modified", - "type": "string", - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "roas_bid": { - "description": "Return on ad spend goal set for the campaign", - "type": ["null", "number"] - }, - "is_smart_performance_campaign": { - "description": "Flag indicating if the campaign uses smart performance optimization", - "type": ["null", "boolean"] - }, - "is_search_campaign": { - "description": "Flag indicating if the campaign is a search campaign", - "type": ["null", "boolean"] - }, - "app_promotion_type": { - "description": "Type of app promotion being used in the campaign", - "type": ["null", "string"] - }, - "rf_campaign_type": { - "description": "Type of RF (reach and frequency) campaign being run", - "type": ["null", "string"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_images.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_images.json deleted file mode 100644 index 4a9a29c0610f..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_images.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "type": "object", - "properties": { - "image_id": { - "description": "The unique identifier for the image.", - "type": ["null", "string"] - }, - "format": { - "description": "The format type of the image file.", - "type": ["null", "string"] - }, - "image_url": { - "description": "The URL to access the image.", - "type": ["null", "string"] - }, - "height": { - "description": "The height dimension of the image.", - "type": ["null", "integer"] - }, - "width": { - "description": "The width dimension of the image.", - "type": ["null", "integer"] - }, - "signature": { - "description": "The signature of the image for security purposes.", - "type": ["null", "string"] - }, - "size": { - "description": "The size of the image file.", - "type": ["null", "integer"] - }, - "material_id": { - "description": "The ID associated with the material of the image.", - "type": ["null", "string"] - }, - "is_carousel_usable": { - "description": "Flag indicating if the image can be used in a carousel.", - "type": ["null", "boolean"] - }, - "file_name": { - "description": "The name of the image file.", - "type": ["null", "string"] - }, - "create_time": { - "description": "The timestamp when the creative asset image was created.", - "type": ["null", "string"], - "format": "date-time" - }, - "modify_time": { - "description": "The timestamp when the creative asset image was last modified.", - "type": ["null", "string"], - "format": "date-time" - }, - "displayable": { - "description": "Flag indicating if the image is displayable or not.", - "type": ["null", "boolean"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_music.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_music.json deleted file mode 100644 index d6acebbdcd5e..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_music.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "type": "object", - "properties": { - "music_id": { - "description": "The unique identifier for the music asset.", - "type": ["null", "string"] - }, - "material_id": { - "description": "The unique ID assigned to the music asset.", - "type": ["null", "string"] - }, - "sources": { - "description": "The list of different sources or versions available for the music asset.", - "type": ["null", "array"], - "items": { - "type": ["null", "string"] - } - }, - "author": { - "description": "The author of the music asset.", - "type": ["null", "string"] - }, - "liked": { - "description": "The number of likes received by the music asset.", - "type": ["null", "boolean"] - }, - "cover_url": { - "description": "The URL to the cover image associated with the music asset.", - "type": ["null", "string"] - }, - "url": { - "description": "The URL to access or play the music asset.", - "type": ["null", "string"] - }, - "duration": { - "description": "The duration of the music asset in seconds.", - "type": ["null", "number"] - }, - "style": { - "description": "The style or genre of the music asset.", - "type": ["null", "string"] - }, - "signature": { - "description": "The digital signature associated with the music asset.", - "type": ["null", "string"] - }, - "name": { - "description": "The name or title of the music asset.", - "type": ["null", "string"] - }, - "file_name": { - "description": "The file name of the music asset.", - "type": ["null", "string"] - }, - "copyright": { - "description": "The copyright information related to the music asset.", - "type": ["null", "string"] - }, - "create_time": { - "description": "The timestamp indicating when the music asset was created.", - "type": ["null", "string"], - "format": "date-time" - }, - "modify_time": { - "description": "The timestamp indicating when the music asset was last modified.", - "type": ["null", "string"], - "format": "date-time" - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_portfolios.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_portfolios.json deleted file mode 100644 index 58544ab4295a..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_portfolios.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "type": "object", - "properties": { - "creative_portfolio_id": { - "description": "The unique identifier for the creative portfolio.", - "type": ["null", "string"] - }, - "creative_portfolio_type": { - "description": "The type of the creative portfolio, such as image, video, or carousel.", - "type": ["null", "string"] - }, - "creative_portfolio_preview_url": { - "description": "The URL pointing to a preview image or video of the creative portfolio.", - "type": ["null", "string"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_videos.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_videos.json deleted file mode 100644 index d809fe3b3f90..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/schemas/creative_assets_videos.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "type": "object", - "properties": { - "video_id": { - "description": "ID of the video.", - "type": ["null", "string"] - }, - "video_cover_url": { - "description": "URL for the cover image of the video.", - "type": ["null", "string"] - }, - "format": { - "description": "Format of the video file.", - "type": ["null", "string"] - }, - "preview_url": { - "description": "URL for previewing the video.", - "type": ["null", "string"] - }, - "preview_url_expire_time": { - "description": "Timestamp when the preview URL expires.", - "type": ["null", "string"], - "format": "date-time", - "airbyte_type": "timestamp_without_timezone" - }, - "duration": { - "description": "Duration of the video in seconds.", - "type": ["null", "number"] - }, - "height": { - "description": "Height of the video in pixels.", - "type": ["null", "integer"] - }, - "width": { - "description": "Width of the video in pixels.", - "type": ["null", "integer"] - }, - "bit_rate": { - "description": "The bitrate of the video.", - "type": ["null", "number"] - }, - "signature": { - "description": "Signature for authenticating the video request.", - "type": ["null", "string"] - }, - "size": { - "description": "Size of the video file in bytes.", - "type": ["null", "integer"] - }, - "material_id": { - "description": "ID of the video material.", - "type": ["null", "string"] - }, - "allowed_placements": { - "description": "List of placements where the video can be used.", - "type": ["null", "array"], - "items": { - "description": "Specific placement where the video is allowed.", - "type": ["null", "string"] - } - }, - "allow_download": { - "description": "Indicates if the video can be downloaded by users.", - "type": ["null", "boolean"] - }, - "file_name": { - "description": "Name of the video file.", - "type": ["null", "string"] - }, - "create_time": { - "description": "Timestamp when the video was created.", - "type": ["null", "string"], - "format": "date-time" - }, - "modify_time": { - "description": "Timestamp when the video was last modified.", - "type": ["null", "string"], - "format": "date-time" - }, - "displayable": { - "description": "Indicates if the video is displayable.", - "type": ["null", "boolean"] - } - } -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/source.py b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/source.py index 71e94d389015..8d0b1f1afcc2 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/source.py +++ b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/source.py @@ -2,236 +2,75 @@ # Copyright (c) 2023 Airbyte, Inc., all rights reserved. # import logging -from typing import Any, List, Mapping, Tuple +from typing import Any, List, Mapping -import pendulum -from airbyte_cdk.models import SyncMode -from airbyte_cdk.sources import AbstractSource +from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource from airbyte_cdk.sources.streams import Stream -from airbyte_cdk.sources.streams.http.auth import TokenAuthenticator - -from .streams import ( - DEFAULT_END_DATE, - DEFAULT_START_DATE, - MINIMUM_START_DATE, - AdGroupAudienceReports, - AdGroupAudienceReportsByCountry, - AdGroupAudienceReportsByPlatform, - AdGroups, - AdGroupsReports, - Ads, - AdsAudienceReports, - AdsAudienceReportsByCountry, - AdsAudienceReportsByPlatform, - AdsAudienceReportsByProvince, - AdsReports, - AdvertiserIds, - Advertisers, - AdvertisersAudienceReports, - AdvertisersAudienceReportsByCountry, - AdvertisersAudienceReportsByPlatform, - AdvertisersReports, - Audiences, - BasicReports, - Campaigns, - CampaignsAudienceReports, - CampaignsAudienceReportsByCountry, - CampaignsAudienceReportsByPlatform, - CampaignsReports, - CreativeAssetsImages, - CreativeAssetsMusic, - CreativeAssetsPortfolios, - CreativeAssetsVideos, - Daily, - Hourly, - Lifetime, - ReportGranularity, -) logger = logging.getLogger("airbyte") DOCUMENTATION_URL = "https://docs.airbyte.com/integrations/sources/tiktok-marketing" +SANDBOX_STREAM_NAMES = [ + "ad_group_audience_reports_by_country_daily", + "ad_group_audience_reports_by_platform_daily", + "ad_group_audience_reports_daily", + "ad_groups", + "ad_groups_reports_daily", + "ad_groups_reports_hourly", + "ad_groups_reports_lifetime", + "ads", + "ads_audience_reports_by_country_daily", + "ads_audience_reports_by_platform_daily", + "ads_audience_reports_by_province_daily", + "ads_audience_reports_daily", + "ads_reports_daily", + "ads_reports_hourly", + "ads_reports_lifetime", + "advertisers", + "audiences", + "campaigns", + "campaigns_audience_reports_by_country_daily", + "campaigns_audience_reports_by_platform_daily", + "campaigns_audience_reports_daily", + "campaigns_reports_daily", + "campaigns_reports_hourly", + "campaigns_reports_lifetime", + "creative_assets_images", + "creative_assets_music", + "creative_assets_portfolios", + "creative_assets_videos", +] +REPORT_GRANULARITY = {"DAY": "daily", "HOUR": "hourly", "LIFETIME": "lifetime"} + + +class SourceTiktokMarketing(YamlDeclarativeSource): + def __init__(self): + super().__init__(**{"path_to_yaml": "manifest.yaml"}) - -def get_report_stream(report: BasicReports, granularity: ReportGranularity) -> BasicReports: - """Fabric method to generate final class with name like: AdsReports + Hourly""" - report_class_name = f"{report.__name__}{granularity.__name__}" - return type(report_class_name, (granularity, report), {}) - - -class TiktokTokenAuthenticator(TokenAuthenticator): - """ - Docs: https://business-api.tiktok.com/marketing_api/docs?rid=sta6fe2yww&id=1701890922708994 - """ - - def __init__(self, token: str, **kwargs): - super().__init__(token, **kwargs) - self.token = token - - def get_auth_header(self) -> Mapping[str, Any]: - return {"Access-Token": self.token} - - -class SourceTiktokMarketing(AbstractSource): @staticmethod - def _prepare_stream_args(config: Mapping[str, Any]) -> Mapping[str, Any]: - """Converts an input configure to stream arguments""" - + def _is_sandbox(config: Mapping[str, Any]) -> bool: credentials = config.get("credentials") - if credentials: - # used for new config format is_sandbox = credentials["auth_type"] == "sandbox_access_token" - access_token = credentials["access_token"] - secret = credentials.get("secret") - app_id = int(credentials.get("app_id", 0)) - advertiser_id = credentials.get("advertiser_id") else: - # old config only has advertiser id in environment object - # if there is a secret it is a prod config - access_token = config["access_token"] secret = config.get("environment", {}).get("secret") is_sandbox = secret is None - app_id = int(config.get("environment", {}).get("app_id", 0)) - advertiser_id = config.get("environment", {}).get("advertiser_id") - - start_date = config.get("start_date") or DEFAULT_START_DATE - if pendulum.parse(start_date) < pendulum.parse(MINIMUM_START_DATE): - logger.warning(f"The start date is too far in the past. Setting it to {MINIMUM_START_DATE}.") - start_date = MINIMUM_START_DATE - stream_args = { - "authenticator": TiktokTokenAuthenticator(access_token), - "start_date": start_date, - "end_date": config.get("end_date") or DEFAULT_END_DATE, - "app_id": app_id, - "secret": secret, - "access_token": access_token, - "is_sandbox": is_sandbox, - "attribution_window": config.get("attribution_window"), - "include_deleted": config.get("include_deleted"), - } - if advertiser_id: - stream_args.update(**{"advertiser_id": advertiser_id}) - - return stream_args - - def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, any]: - """ - Tests if the input configuration can be used to successfully connect to the integration - """ - try: - advertisers = Advertisers(**self._prepare_stream_args(config)) - for slice_ in advertisers.stream_slices(): - next(advertisers.read_records(SyncMode.full_refresh, stream_slice=slice_)) - except Exception as err: - return False, err - return True, None + return is_sandbox def streams(self, config: Mapping[str, Any]) -> List[Stream]: - args = self._prepare_stream_args(config) - - is_production = not (args["is_sandbox"]) - - report_granularity = config.get("report_granularity") - - # 1. Basic streams: - streams = [ - Advertisers(**args), - Ads(**args), - AdGroups(**args), - Audiences(**args), - Campaigns(**args), - CreativeAssetsImages(**args), - CreativeAssetsMusic(**args), - CreativeAssetsPortfolios(**args), - CreativeAssetsVideos(**args), - ] - - if is_production: - streams.append(AdvertiserIds(**args)) - - # Report streams in different connector version: - # for < 0.1.13 - expose report streams initialized with 'report_granularity' argument, like: - # AdsReports(report_granularity='DAILY') - # AdsReports(report_granularity='LIFETIME') - # for >= 0.1.13 - expose report streams in format: _, like: - # AdsReportsDaily(Daily, AdsReports) - # AdsReportsLifetime(Lifetime, AdsReports) - - if report_granularity: - # for version < 0.1.13 - compatibility with old config with 'report_granularity' option - - # 2. Basic report streams: - report_args = dict(report_granularity=report_granularity, **args) - streams.extend( - [ - AdsReports(**report_args), - AdGroupsReports(**report_args), - CampaignsReports(**report_args), - ] - ) - - # 3. Audience report streams: - if not report_granularity == ReportGranularity.LIFETIME: - # https://ads.tiktok.com/marketing_api/docs?id=1707957217727489 - # Audience report only supports lifetime metrics at the ADVERTISER level. - streams.extend( - [ - AdsAudienceReports(**report_args), - AdGroupAudienceReports(**report_args), - CampaignsAudienceReportsByCountry(**report_args), - ] - ) - - # 4. streams work only in prod env - if is_production: - streams.extend( - [ - AdvertisersReports(**report_args), - AdvertisersAudienceReports(**report_args), - ] - ) - - else: - # for version >= 0.1.13: - - # 2. Basic report streams: - reports = [AdsReports, AdGroupsReports, CampaignsReports] - audience_reports = [ - AdsAudienceReports, - AdsAudienceReportsByCountry, - AdsAudienceReportsByPlatform, - AdsAudienceReportsByProvince, - AdGroupAudienceReports, - AdGroupAudienceReportsByCountry, - AdGroupAudienceReportsByPlatform, - CampaignsAudienceReports, - CampaignsAudienceReportsByCountry, - CampaignsAudienceReportsByPlatform, - ] - if is_production: - # 2.1 streams work only in prod env - reports.append(AdvertisersReports) - audience_reports.extend( - [ - AdvertisersAudienceReports, - AdvertisersAudienceReportsByCountry, - AdvertisersAudienceReportsByPlatform, - ] - ) - - for Report in reports: - for Granularity in [Hourly, Daily, Lifetime]: - streams.append(get_report_stream(Report, Granularity)(**args)) - # add a for loop here for the other dimension to split by - - # 3. Audience report streams: - for Report in audience_reports: - # As per TikTok's documentation, audience reports only support daily (not hourly) time dimension for metrics - streams.append(get_report_stream(Report, Daily)(**args)) - - # Audience report supports lifetime metrics only at the ADVERTISER level (see 2.1). - if Report == AdvertisersAudienceReports: - streams.append(get_report_stream(Report, Lifetime)(**args)) + granularity = config.get("report_granularity") + streams = super().streams(config) + + if self._is_sandbox(config): + streams = [stream for stream in streams if stream.name in SANDBOX_STREAM_NAMES] + + if granularity: + granularity_streams = [] + for stream in streams: + if "report" not in stream.name or REPORT_GRANULARITY[granularity] in stream.name: + # old configs with provided report granularity don't have granularity in stream name + stream.name = stream.name.replace(f"_{REPORT_GRANULARITY[granularity]}", "") + granularity_streams.append(stream) + return granularity_streams return streams diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/spec.json b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/spec.json deleted file mode 100644 index 5751d01fdedb..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/spec.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "documentationUrl": "https://docs.airbyte.com/integrations/sources/tiktok-marketing", - "changelogUrl": "https://docs.airbyte.com/integrations/sources/tiktok-marketing", - "connectionSpecification": { - "title": "TikTok Marketing Source Spec", - "type": "object", - "properties": { - "credentials": { - "title": "Authentication Method", - "description": "Authentication method", - "default": {}, - "order": 0, - "type": "object", - "oneOf": [ - { - "title": "OAuth2.0", - "type": "object", - "properties": { - "auth_type": { - "title": "Auth Type", - "const": "oauth2.0", - "order": 0, - "type": "string" - }, - "app_id": { - "title": "App ID", - "description": "The Developer Application App ID.", - "airbyte_secret": true, - "type": "string" - }, - "secret": { - "title": "Secret", - "description": "The Developer Application Secret.", - "airbyte_secret": true, - "type": "string" - }, - "access_token": { - "title": "Access Token", - "description": "Long-term Authorized Access Token.", - "airbyte_secret": true, - "type": "string" - }, - "advertiser_id": { - "title": "Advertiser ID", - "description": "The Advertiser ID to filter reports and streams. Let this empty to retrieve all.", - "type": "string" - } - }, - "required": ["app_id", "secret", "access_token"] - }, - { - "title": "Sandbox Access Token", - "type": "object", - "properties": { - "auth_type": { - "title": "Auth Type", - "const": "sandbox_access_token", - "order": 0, - "type": "string" - }, - "advertiser_id": { - "title": "Advertiser ID", - "description": "The Advertiser ID which generated for the developer's Sandbox application.", - "type": "string" - }, - "access_token": { - "title": "Access Token", - "description": "The long-term authorized access token.", - "airbyte_secret": true, - "type": "string" - } - }, - "required": ["advertiser_id", "access_token"] - } - ] - }, - "start_date": { - "title": "Replication Start Date", - "description": "The Start Date in format: YYYY-MM-DD. Any data before this date will not be replicated. If this parameter is not set, all data will be replicated.", - "default": "2016-09-01", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", - "order": 1, - "type": "string", - "format": "date" - }, - "end_date": { - "title": "End Date", - "description": "The date until which you'd like to replicate data for all incremental streams, in the format YYYY-MM-DD. All data generated between start_date and this date will be replicated. Not setting this option will result in always syncing the data till the current date.", - "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", - "order": 2, - "type": "string", - "format": "date" - }, - "attribution_window": { - "title": "Attribution Window", - "description": "The attribution window in days.", - "minimum": 0, - "maximum": 364, - "default": 3, - "order": 3, - "type": "integer" - }, - "include_deleted": { - "title": "Include Deleted Data in Reports", - "description": "Set to active if you want to include deleted data in reports.", - "default": false, - "order": 4, - "type": "boolean" - } - } - }, - "supportsIncremental": true, - "supported_destination_sync_modes": ["overwrite", "append", "append_dedup"], - "advanced_auth": { - "auth_flow_type": "oauth2.0", - "predicate_key": ["credentials", "auth_type"], - "predicate_value": "oauth2.0", - "oauth_config_specification": { - "complete_oauth_output_specification": { - "title": "CompleteOauthOutputSpecification", - "type": "object", - "properties": { - "access_token": { - "title": "Access Token", - "path_in_connector_config": ["credentials", "access_token"], - "type": "string" - } - }, - "required": ["access_token"] - }, - "complete_oauth_server_input_specification": { - "title": "CompleteOauthServerInputSpecification", - "type": "object", - "properties": { - "app_id": { - "title": "App Id", - "type": "string" - }, - "secret": { - "title": "Secret", - "type": "string" - } - }, - "required": ["app_id", "secret"] - }, - "complete_oauth_server_output_specification": { - "title": "CompleteOauthServerOutputSpecification", - "type": "object", - "properties": { - "app_id": { - "title": "App Id", - "path_in_connector_config": ["credentials", "app_id"], - "type": "string" - }, - "secret": { - "title": "Secret", - "path_in_connector_config": ["credentials", "secret"], - "type": "string" - } - }, - "required": ["app_id", "secret"] - } - } - }, - "additionalProperties": true -} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/streams.py b/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/streams.py deleted file mode 100644 index e42a67f25602..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/source_tiktok_marketing/streams.py +++ /dev/null @@ -1,941 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - - -import json -from abc import ABC, abstractmethod -from datetime import datetime -from decimal import Decimal -from enum import Enum -from functools import total_ordering -from typing import Any, Dict, Iterable, List, Mapping, MutableMapping, Optional, Tuple, TypeVar, Union - -import pendulum -import pydantic -import requests -from airbyte_cdk.models import SyncMode -from airbyte_cdk.sources.streams.core import package_name_from_class -from airbyte_cdk.sources.streams.http import HttpStream -from airbyte_cdk.sources.utils.schema_helpers import ResourceSchemaLoader -from airbyte_cdk.sources.utils.transform import TransformConfig, TypeTransformer - -# TikTok Initial release date is September 2016 -DEFAULT_START_DATE = "2016-09-01" -MINIMUM_START_DATE = "2012-01-01" -DEFAULT_END_DATE = str(datetime.now().date()) -NOT_AUDIENCE_METRICS = [ - "reach", - "cost_per_1000_reached", - "frequency", - "secondary_goal_result", - "cost_per_secondary_goal_result", - "secondary_goal_result_rate", - "cash_spend", - "voucher_spend", - "video_play_actions", - "video_watched_2s", - "video_watched_6s", - "average_video_play", - "average_video_play_per_user", - "video_views_p25", - "video_views_p50", - "video_views_p75", - "video_views_p100", - "profile_visits", - "likes", - "comments", - "shares", - "follows", - "clicks_on_music_disc", - "real_time_app_install", - "real_time_app_install_cost", - "app_install", - "total_purchase_value", - "total_onsite_shopping_value", - "onsite_shopping", - "vta_purchase", - "cta_purchase", - "vta_conversion", - "cta_conversion", - "total_pageview", - "complete_payment", - "value_per_complete_payment", - "total_complete_payment_rate", - "profile_visits_rate", - "purchase", - "purchase_rate", - "registration", - "registration_rate", - "sales_lead", - "sales_lead_rate", - "cost_per_app_install", - "cost_per_purchase", - "cost_per_registration", - "total_purchase_value", - "cost_per_sales_lead", - "cost_per_total_sales_lead", - "cost_per_total_app_event_add_to_cart", - "total_app_event_add_to_cart", -] - -T = TypeVar("T") - - -# Hierarchy of classes -# TiktokStream -# ├─AdvertiserIds AdvertiserIds -# └─FullRefreshTiktokStream -# ├─Advertisers (1 advertisers) -# └─IncrementalTiktokStream -# ├─AdGroups (2 ad_groups) -# ├─Ads (3 ads) -# ├─Campaigns (4 campaigns) -# └─BasicReports -# ├─AdsReports (5 ads_reports) -# ├─AdvertisersReports (6 advertisers_reports) -# ├─CampaignsReports (7 campaigns_reports) -# ├─AdGroupsReports (8 ad_groups_reports) -# └─AudienceReport -# ├─AdGroupAudienceReports (9 ad_group_audience_reports) -# | ├─AdGroupAudienceReportsByCountry (10 ad_group_audience_reports_by_country) -# | └─AdGroupAudienceReportsByPlatform (11 ad_group_audience_reports_by_platform) -# ├─AdsAudienceReports (12 ads_audience_reports) -# | ├─AdsAudienceReportsByCountry (13 ads_audience_reports_by_country) -# | ├─AdsAudienceReportsByPlatform (14 ads_audience_reports_by_platform) -# | ├─AdsAudienceReportsByProvince (14 ads_audience_reports_by_platform) -# ├─AdvertisersAudienceReports (15 advertisers_audience_reports) -# | ├─AdvertisersAudienceReportsByCountry (16 advertisers_audience_reports_by_country) -# | └─AdvertisersAudienceReportsByPlatform (17 advertisers_audience_reports_by_platform) -# └─CampaignsAudienceReports (18 campaigns_audience_reports) -# ├─CampaignsAudienceReportsByCountry (19 campaigns_audience_reports_by_country) -# └─CampaignsAudienceReportsByPlatform (20 campaigns_audience_reports_by_platform) - - -@total_ordering -class JsonUpdatedState(pydantic.BaseModel): - current_stream_state: str - stream: T - - def __repr__(self): - """Overrides print view""" - return str(self.dict()) - - def dict(self, **kwargs): - """Overrides default logic. - A new updated stage has to be sent if all advertisers are used only - """ - if not self.stream.is_finished: - return self.current_stream_state - max_updated_at = self.stream.max_cursor_date or "" - return max(max_updated_at, self.current_stream_state) - - def __eq__(self, other): - if isinstance(other, JsonUpdatedState): - return self.current_stream_state == other.current_stream_state - return self.current_stream_state == other - - def __lt__(self, other): - if isinstance(other, JsonUpdatedState): - return self.current_stream_state < other.current_stream_state - return self.current_stream_state < other - - -class ReportLevel(str, Enum): - ADVERTISER = "ADVERTISER" - CAMPAIGN = "CAMPAIGN" - ADGROUP = "ADGROUP" - AD = "AD" - - -class ReportGranularity(str, Enum): - LIFETIME = "LIFETIME" - DAY = "DAY" - HOUR = "HOUR" - - @classmethod - def default(cls): - return cls.DAY - - -class Hourly: - report_granularity = ReportGranularity.HOUR - - -class Daily: - report_granularity = ReportGranularity.DAY - - -class Lifetime: - report_granularity = ReportGranularity.LIFETIME - - -class TiktokException(Exception): - """default exception for custom Tiktok logic""" - - -class TiktokStream(HttpStream, ABC): - # endpoints can have different list names - response_list_field = "list" - - # max value of page - page_size = 1000 - - def __init__(self, **kwargs): - super().__init__(authenticator=kwargs.get("authenticator")) - - self._advertiser_id = kwargs.get("advertiser_id") - self.is_sandbox = kwargs.get("is_sandbox") - - def parse_response(self, response: requests.Response, **kwargs) -> Iterable[Mapping]: - """All responses have the similar structure: - { - "message": "", - "code": , # 0 if error else error unique code - "request_id": "" - "data": { - "page_info": { - "total_number": , - "page": , - "page_size": , - "total_page": - }, - "list": [ - - ] - } - } - """ - data = response.json() - if data["code"]: - raise TiktokException(data) - data = data["data"] - if self.response_list_field in data: - data = data[self.response_list_field] - for record in data: - yield record - - @property - def url_base(self) -> str: - """ - Docs: https://business-api.tiktok.com/marketing_api/docs?id=1701890920013825 - """ - if self.is_sandbox: - return "https://sandbox-ads.tiktok.com/open_api/v1.3/" - return "https://business-api.tiktok.com/open_api/v1.3/" - - def next_page_token(self, *args, **kwargs) -> Optional[Mapping[str, Any]]: - # this data without listing - return None - - def should_retry(self, response: requests.Response) -> bool: - """ - Once the rate limit is met, the server returns "code": 40100 - Docs: https://business-api.tiktok.com/marketing_api/docs?id=1701890997610497 - Retry 50002 as well - it's a server error. - Retry when 504 error: response doesn't consist json, so we need to handle response status code to retry. - """ - try: - data = response.json() - except Exception: - if response.status_code == 504: - self.logger.error("Gateway Timeout: The proxy server did not receive a timely response from the upstream server.") - return super().should_retry(response) - self.logger.error(f"Incorrect JSON response: {response.text}") - raise - if data["code"] in (40100, 50002): - return True - return super().should_retry(response) - - def backoff_time(self, response: requests.Response) -> Optional[float]: - """ - The system uses a second call limit for each developer app. The set limit varies according to the app's call limit level. - """ - # Basic: 10/sec - # Advanced: 20/sec - # Premium: 30/sec - # All apps are set to basic call limit level by default. - # Returns maximum possible delay - return 0.6 - - -class AdvertiserIds(TiktokStream): - """Loading of all possible advertiser ids""" - - primary_key = "advertiser_id" - use_cache = True # it is used in all streams - - transformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization) - - def __init__(self, authenticator, app_id: int, secret: str, **kwargs): - super().__init__(authenticator=authenticator, advertiser_id=0) - - # for Production env - self._secret = secret - self._app_id = app_id - - def request_params(self, **kwargs) -> MutableMapping[str, Any]: - return {"secret": self._secret, "app_id": self._app_id} - - def path(self, *args, **kwargs) -> str: - return "oauth2/advertiser/get/" - - -class FullRefreshTiktokStream(TiktokStream, ABC): - primary_key = "id" - fields: List[str] = None - - transformer = TypeTransformer(TransformConfig.DefaultSchemaNormalization | TransformConfig.CustomSchemaNormalization) - - @transformer.registerCustomTransform - def transform_function(original_value: Any, field_schema: Dict[str, Any]) -> Any: - """Custom transformation""" - if original_value == "-": - return None - elif isinstance(original_value, float): - return Decimal(original_value) - return original_value - - def __init__(self, start_date: str, end_date: str, **kwargs): - super().__init__(**kwargs) - self.kwargs = kwargs - # convert a start date to TikTok format - # example: "2021-08-24" => "2021-08-24 00:00:00" - self._start_time = pendulum.parse(start_date or DEFAULT_START_DATE).strftime("%Y-%m-%d 00:00:00") - # convert end date to TikTok format - # example: "2021-08-24" => "2021-08-24 00:00:00" - self._end_time = pendulum.parse(end_date or DEFAULT_END_DATE).strftime("%Y-%m-%d 00:00:00") - self.max_cursor_date = None - self._advertiser_ids = [] - - @staticmethod - def convert_array_param(arr: List[Union[str, int]]) -> str: - return json.dumps(arr) - - def get_advertiser_ids(self) -> List[int]: - if self._advertiser_id: - # for sandbox: just return advertiser_id provided in spec - # for production: it will filter only the advertiser id provied in spec - ids = [self._advertiser_id] - else: - # for prod: return list of all available ids from AdvertiserIds stream if the field is empty - # in the connector configuration - advertiser_ids = AdvertiserIds(**self.kwargs).read_records(sync_mode=SyncMode.full_refresh) - ids = [advertiser["advertiser_id"] for advertiser in advertiser_ids] - - self._advertiser_ids = ids - return ids - - def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]: - """Each stream slice is for separate advertiser id""" - self.get_advertiser_ids() - while self._advertiser_ids: - # self._advertiser_ids need to be exhausted so that JsonUpdatedState knows - # when all stream slices are processed (stream.is_finished) - advertiser_id = self._advertiser_ids.pop(0) - yield {"advertiser_id": advertiser_id} - - @property - def is_finished(self): - return len(self._advertiser_ids) == 0 - - def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]: - """All responses have the following pagination data: - { - "data": { - "page_info": { - "total_number": < total_item_count >, - "page": < current_page_number >, - "page_size": < page_size >, - "total_page": < total_page_count > - }, - ... - } - } - """ - - page_info = response.json().get("data", {}).get("page_info", {}) - if not page_info: - return None - if page_info["page"] < page_info["total_page"]: - return {"page": page_info["page"] + 1} - return None - - def request_params( - self, - stream_state: Mapping[str, Any] = None, - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> MutableMapping[str, Any]: - params = {"page_size": self.page_size} - if self.fields: - params["fields"] = self.convert_array_param(self.fields) - if stream_slice: - params.update(stream_slice) - if next_page_token: - params.update(next_page_token) - return params - - -class IncrementalTiktokStream(FullRefreshTiktokStream, ABC): - cursor_field = "modify_time" - - def select_cursor_field_value(self, data: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None) -> str: - if not data or not self.cursor_field: - return None - - cursor_field_path = self.cursor_field if isinstance(self.cursor_field, list) else [self.cursor_field] - - # backward capability to support old state objects - if "dimensions" in data: - cursor_field_path = self.deprecated_cursor_field - - result = data - for key in cursor_field_path: - result = result.get(key) - return result - - def unnest_cursor_and_pk(self, record: Mapping[str, Any]): - """ - unnest nested cursor_field and primary_key from nested `dimensions` object to root-level for *_reports streams - """ - - def to_list(s): - if not isinstance(s, list): - s = [s] - return s - - dimensions = record.get("dimensions", {}) - fields = to_list(self.cursor_field) + to_list(self.primary_key) - for field in fields: - if field in dimensions: - record[field] = dimensions.get(field) - return record - - def parse_response( - self, response: requests.Response, stream_state: Mapping[str, Any], stream_slice: Mapping[str, Any] = None, **kwargs - ) -> Iterable[Mapping]: - """Additional data filtering""" - state_cursor_value = self.select_cursor_field_value(stream_state) or self._start_time - for record in super().parse_response(response=response, stream_state=stream_state, **kwargs): - record = self.unnest_cursor_and_pk(record) - updated_cursor_value = self.select_cursor_field_value(record, stream_slice) - if updated_cursor_value is None: - yield record - elif updated_cursor_value < state_cursor_value: - continue - else: - if not self.max_cursor_date or self.max_cursor_date < updated_cursor_value: - self.max_cursor_date = updated_cursor_value - yield record - - def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]: - if not self.cursor_field: - # BasicReports streams are incremental. However, report streams configured to use LIFETIME granularity only work as - # full refresh and don't have a cursor field. There is no state value to extract from the record - return {} - - # needs to save a last state if all advertisers are used before only - current_stream_state_value = (self.select_cursor_field_value(current_stream_state)) or "" - - # a object JsonUpdatedState is related with a current stream and should return a new updated state if needed - if not isinstance(current_stream_state_value, JsonUpdatedState): - current_stream_state_value = JsonUpdatedState(stream=self, current_stream_state=current_stream_state_value) - - # reports streams have cursor fields which be allocated into a nested object - cursor_field_path = self.cursor_field if isinstance(self.cursor_field, list) else [self.cursor_field] - # generate a dict with nested items - # ["key1", "key1"] => {"key1": {"key2": }} - tree_dict = current_stream_state_value - for key in reversed(cursor_field_path): - tree_dict = {key: tree_dict} - return tree_dict - - -class Advertisers(FullRefreshTiktokStream): - """Docs: https://ads.tiktok.com/marketing_api/docs?id=1739593083610113""" - - primary_key = "advertiser_id" - - def request_params( - self, - stream_state: Mapping[str, Any] = None, - stream_slice: Mapping[str, Any] = None, - next_page_token: Mapping[str, Any] = None, - ) -> MutableMapping[str, Any]: - stream_slice = stream_slice or {} - return {key: self.convert_array_param(value) for key, value in stream_slice.items()} - - def path(self, *args, **kwargs) -> str: - return "advertiser/info/" - - def stream_slices(self, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]: - ids = self.get_advertiser_ids() - start, end, step = 0, len(ids), 100 - for i in range(start, end, step): - yield {"advertiser_ids": ids[i : min(end, i + step)]} - - -class Audiences(FullRefreshTiktokStream): - """Docs: https://business-api.tiktok.com/portal/docs?id=1739940506015746""" - - page_size = 100 - primary_key = "audience_id" - - def path(self, *args, **kwargs) -> str: - return "dmp/custom_audience/list/" - - -class CreativeAssetsMusic(FullRefreshTiktokStream): - """Docs: https://business-api.tiktok.com/portal/docs?id=1740053909509122""" - - primary_key = "music_id" - response_list_field = "musics" - - def path(self, *args, **kwargs) -> str: - return "file/music/get/" - - -class CreativeAssetsPortfolios(FullRefreshTiktokStream): - """Docs: https://business-api.tiktok.com/portal/docs?id=1766324010279938""" - - page_size = 100 - primary_key = "creative_portfolio_id" - response_list_field = "creative_portfolios" - - def path(self, *args, **kwargs) -> str: - return "creative/portfolio/list/" - - -class Campaigns(IncrementalTiktokStream): - """Docs: https://ads.tiktok.com/marketing_api/docs?id=1739315828649986""" - - primary_key = "campaign_id" - - def path(self, *args, **kwargs) -> str: - return "campaign/get/" - - -class AdGroups(IncrementalTiktokStream): - """Docs: https://ads.tiktok.com/marketing_api/docs?id=1739314558673922""" - - primary_key = "adgroup_id" - - def path(self, *args, **kwargs) -> str: - return "adgroup/get/" - - -class Ads(IncrementalTiktokStream): - """Docs: https://ads.tiktok.com/marketing_api/docs?id=1735735588640770""" - - primary_key = "ad_id" - - def path(self, *args, **kwargs) -> str: - return "ad/get/" - - -class CreativeAssetsImages(IncrementalTiktokStream): - """Docs: https://business-api.tiktok.com/portal/docs?id=1740052016789506""" - - page_size = 100 - primary_key = "image_id" - - def path(self, *args, **kwargs) -> str: - return "file/image/ad/search/" - - -class CreativeAssetsVideos(IncrementalTiktokStream): - """Docs: https://business-api.tiktok.com/portal/docs?id=1740050472224769""" - - page_size = 100 - primary_key = "video_id" - - def path(self, *args, **kwargs) -> str: - return "file/video/ad/search/" - - -class BasicReports(IncrementalTiktokStream, ABC): - """Docs: https://ads.tiktok.com/marketing_api/docs?id=1738864915188737""" - - schema_name = "basic_reports" - report_granularity = None - - spec_id_dimensions = { - ReportLevel.ADVERTISER: "advertiser_id", - ReportLevel.CAMPAIGN: "campaign_id", - ReportLevel.ADGROUP: "adgroup_id", - ReportLevel.AD: "ad_id", - } - - spec_time_dimensions = { - ReportGranularity.DAY: "stat_time_day", - ReportGranularity.HOUR: "stat_time_hour", - } - - @property - def primary_key(self) -> Optional[Union[str, List[str], List[List[str]]]]: - return self._get_reporting_dimensions() - - def __init__(self, **kwargs): - report_granularity = kwargs.pop("report_granularity", None) - self.attribution_window = kwargs.get("attribution_window") or 0 - self.include_deleted = kwargs.get("include_deleted", False) - super().__init__(**kwargs) - - # Important: - # for >= 0.1.13 - granularity is set via inheritance - # for < 0.1.13 - granularity is set via init param - if report_granularity: - self.report_granularity = report_granularity - - @property - def filters(self) -> List[MutableMapping[str, Any]]: - if self.include_deleted: - return [ - {"filter_value": ["STATUS_ALL"], "field_name": "ad_status", "filter_type": "IN"}, - {"filter_value": ["STATUS_ALL"], "field_name": "campaign_status", "filter_type": "IN"}, - {"filter_value": ["STATUS_ALL"], "field_name": "adgroup_status", "filter_type": "IN"}, - ] - return [] - - @property - @abstractmethod - def report_level(self) -> ReportLevel: - """ - Returns a necessary level value - """ - - @property - def deprecated_cursor_field(self): - if self.report_granularity == ReportGranularity.DAY: - return ["dimensions", "stat_time_day"] - if self.report_granularity == ReportGranularity.HOUR: - return ["dimensions", "stat_time_hour"] - if self.report_granularity == ReportGranularity.LIFETIME: - return ["dimensions", "stat_time_day"] - - @property - def cursor_field(self): - return self.spec_time_dimensions.get(self.report_granularity, []) - - @staticmethod - def _get_time_interval( - start_date: Union[datetime, str], - ending_date: Union[datetime, str], - granularity: ReportGranularity, - attr_window: int = 0, - ) -> Iterable[Tuple[datetime, datetime]]: - """Due to time range restrictions based on the level of granularity of reports, we have to chunk API calls in order - to get the desired time range. - Docs: https://ads.tiktok.com/marketing_api/docs?id=1714590313280513 - :param start_date - Timestamp from which we should start the report - :param granularity - Level of granularity of the report; one of [HOUR, DAY, LIFETIME] - :param atttr_window - The attribution window in days - :return Iterator for pair of start_date and end_date that can be used as request parameters - """ - if isinstance(start_date, str): - start_date = pendulum.parse(start_date).subtract(days=attr_window) - elif isinstance(start_date, datetime): - start_date = start_date.subtract(days=attr_window) - - end_date = pendulum.parse(ending_date) if ending_date else pendulum.now() - - # TikTok API only allows certain amount of days of data based on the reporting granularity - if granularity == ReportGranularity.DAY: - max_interval = 30 - elif granularity == ReportGranularity.HOUR: - max_interval = 1 - elif granularity == ReportGranularity.LIFETIME: - max_interval = 364 - else: - raise ValueError(f"Unsupported reporting granularity: {granularity}, must be one of DAY, HOUR, LIFETIME") - - # for incremental sync with abnormal state produce at least one state message - # by producing at least one stream slice from today - if end_date < start_date: - start_date = end_date - - total_date_diff = end_date - start_date - - iterations = total_date_diff.days // max_interval - - for i in range(iterations + 1): - chunk_start = start_date + pendulum.duration(days=(i * max_interval)) - chunk_end = min(chunk_start + pendulum.duration(days=max_interval, seconds=-1), end_date) - yield chunk_start, chunk_end - - def _get_reporting_dimensions(self): - result = [self.spec_id_dimensions[self.report_level]] - if self.report_granularity in self.spec_time_dimensions: - result.append(self.spec_time_dimensions[self.report_granularity]) - return result - - def _get_metrics(self): - # common metrics for all reporting levels - result = [ - "spend", - "cpc", - "cpm", - "impressions", - "clicks", - "ctr", - "reach", - "cost_per_1000_reached", - "frequency", - "video_play_actions", - "video_watched_2s", - "video_watched_6s", - "average_video_play", - "average_video_play_per_user", - "video_views_p25", - "video_views_p50", - "video_views_p75", - "video_views_p100", - "profile_visits", - "likes", - "comments", - "shares", - "follows", - "clicks_on_music_disc", - "real_time_app_install", - "real_time_app_install_cost", - "app_install", - ] - - if self.report_level == ReportLevel.ADVERTISER and self.report_granularity == ReportGranularity.DAY: - # https://ads.tiktok.com/marketing_api/docs?id=1707957200780290 - result.extend(["cash_spend", "voucher_spend"]) - - if self.report_level in (ReportLevel.CAMPAIGN, ReportLevel.ADGROUP, ReportLevel.AD): - result.extend(["campaign_name"]) - - if self.report_level in (ReportLevel.ADGROUP, ReportLevel.AD): - result.extend( - [ - "campaign_id", - "adgroup_name", - "placement_type", - "tt_app_id", - "tt_app_name", - "mobile_app_id", - "promotion_type", - "dpa_target_audience_type", - ] - ) - - result.extend( - [ - "conversion", - "cost_per_conversion", - "conversion_rate", - "real_time_conversion", - "real_time_cost_per_conversion", - "real_time_conversion_rate", - "result", - "cost_per_result", - "result_rate", - "real_time_result", - "real_time_cost_per_result", - "real_time_result_rate", - "secondary_goal_result", - "cost_per_secondary_goal_result", - "secondary_goal_result_rate", - ] - ) - - if self.report_level == ReportLevel.AD: - result.extend( - [ - "adgroup_id", - "ad_name", - "ad_text", - "total_purchase_value", - "total_onsite_shopping_value", - "onsite_shopping", - "vta_purchase", - "vta_conversion", - "cta_purchase", - "cta_conversion", - "total_pageview", - "complete_payment", - "value_per_complete_payment", - "total_complete_payment_rate", - ] - ) - - return result - - def stream_slices(self, stream_state: Mapping[str, Any] = None, **kwargs) -> Iterable[Optional[Mapping[str, Any]]]: - stream_start = self.select_cursor_field_value(stream_state) or self._start_time - stream_end = self._end_time - - for slice_adv_id in super().stream_slices(**kwargs): - for start_date, end_date in self._get_time_interval(stream_start, stream_end, self.report_granularity, self.attribution_window): - slice = { - "advertiser_id": slice_adv_id["advertiser_id"], - "start_date": start_date.strftime("%Y-%m-%d"), - "end_date": end_date.strftime("%Y-%m-%d"), - } - self.logger.debug( - f'name: {self.name}, advertiser_id: {slice["advertiser_id"]}, slice: {slice["start_date"]} - {slice["end_date"]}' - ) - yield slice - - def path(self, *args, **kwargs) -> str: - return "report/integrated/get/" - - def request_params( - self, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs - ) -> MutableMapping[str, Any]: - params = super().request_params(stream_state=stream_state, stream_slice=stream_slice, **kwargs) - - params["advertiser_id"] = stream_slice["advertiser_id"] - params["service_type"] = "AUCTION" - params["report_type"] = "BASIC" - params["data_level"] = f"AUCTION_{self.report_level}" - params["dimensions"] = json.dumps(self._get_reporting_dimensions()) - params["metrics"] = json.dumps(self._get_metrics()) - if self.report_granularity == ReportGranularity.LIFETIME: - params["lifetime"] = "true" - else: - params["start_date"] = stream_slice["start_date"] - params["end_date"] = stream_slice["end_date"] - - if self.filters: - params["filters"] = json.dumps(self.filters) - return params - - def get_json_schema(self) -> Mapping[str, Any]: - """All reports have same schema""" - return ResourceSchemaLoader(package_name_from_class(AdvertiserIds)).get_schema(self.schema_name) - - def select_cursor_field_value(self, data: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None) -> str: - if stream_slice: - return stream_slice["end_date"] - return super().select_cursor_field_value(data) - - -class AdsReports(BasicReports): - """Custom reports for ads""" - - ref_pk = "ad_id" - report_level = ReportLevel.AD - - -class AdvertisersReports(BasicReports): - """Custom reports for advertiser""" - - ref_pk = "advertiser_id" - report_level = ReportLevel.ADVERTISER - - -class CampaignsReports(BasicReports): - """Custom reports for campaigns""" - - ref_pk = "campaign_id" - report_level = ReportLevel.CAMPAIGN - - -class AdGroupsReports(BasicReports): - """Custom reports for adgroups""" - - ref_pk = "adgroup_id" - report_level = ReportLevel.ADGROUP - - -class AudienceReport(BasicReports, ABC): - """Docs: https://ads.tiktok.com/marketing_api/docs?id=1738864928947201""" - - audience_dimensions: List = ["gender", "age"] - schema_name = "audience_reports" - - def _get_metrics(self): - result = super()._get_metrics() - result = [e for e in result if e not in NOT_AUDIENCE_METRICS] - return result - - def _get_reporting_dimensions(self): - result = super()._get_reporting_dimensions() - result += self.audience_dimensions - return result - - def request_params( - self, stream_state: Mapping[str, Any] = None, stream_slice: Mapping[str, Any] = None, **kwargs - ) -> MutableMapping[str, Any]: - params = super().request_params(stream_state=stream_state, stream_slice=stream_slice, **kwargs) - params["report_type"] = "AUDIENCE" - return params - - -class CampaignsAudienceReports(AudienceReport): - ref_pk = "campaign_id" - report_level = ReportLevel.CAMPAIGN - - -class AdGroupAudienceReports(AudienceReport): - ref_pk = "adgroup_id" - report_level = ReportLevel.ADGROUP - - -class AdsAudienceReports(AudienceReport): - ref_pk = "ad_id" - report_level = ReportLevel.AD - - -class AdvertisersAudienceReports(AudienceReport): - ref_pk = "advertiser_id" - report_level = ReportLevel.ADVERTISER - - -class CampaignsAudienceReportsByCountry(CampaignsAudienceReports): - """Custom reports for campaigns by country""" - - audience_dimensions = ["country_code"] - - -class AdGroupAudienceReportsByCountry(AdGroupAudienceReports): - """Custom reports for adgroups by country""" - - audience_dimensions = ["country_code"] - - -class AdsAudienceReportsByCountry(AdsAudienceReports): - """Custom reports for ads by country""" - - audience_dimensions = ["country_code"] - - -class AdvertisersAudienceReportsByCountry(AdvertisersAudienceReports): - """Custom reports for advertisers by country""" - - audience_dimensions = ["country_code"] - - -class CampaignsAudienceReportsByPlatform(CampaignsAudienceReports): - """Custom reports for campaigns by platform""" - - audience_dimensions = ["platform"] - - -class AdGroupAudienceReportsByPlatform(AdGroupAudienceReports): - """Custom reports for adgroups by platform""" - - audience_dimensions = ["platform"] - - -class AdsAudienceReportsByPlatform(AdsAudienceReports): - """Custom reports for ads by platform""" - - audience_dimensions = ["platform"] - - -class AdvertisersAudienceReportsByPlatform(AdvertisersAudienceReports): - """Custom reports for advertisers by platform""" - - audience_dimensions = ["platform"] - - -class AdsAudienceReportsByProvince(AdsAudienceReports): - """Custom reports for ads by province""" - - audience_dimensions = ["province_id"] diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/conftest.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/conftest.py index 969410ee24e1..4cd06a02eaf2 100644 --- a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/conftest.py +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/conftest.py @@ -3,9 +3,6 @@ import os import pytest -from airbyte_cdk.utils.constants import ENV_REQUEST_CACHE_PATH - -os.environ[ENV_REQUEST_CACHE_PATH] = ENV_REQUEST_CACHE_PATH @pytest.fixture(autouse=True) @@ -13,6 +10,13 @@ def patch_sleep(mocker): mocker.patch("time.sleep") -@pytest.fixture(autouse=True) -def disable_cache(mocker): - mocker.patch("source_tiktok_marketing.streams.AdvertiserIds.use_cache", new_callable=mocker.PropertyMock, return_value=False) +@pytest.fixture(name="config") +def config_fixture(): + config = { + "account_id": 123, + "access_token": "TOKEN", + "start_date": "2019-10-10T00:00:00", + "end_date": "2020-10-10T00:00:00", + } + return config + diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/advetiser_slices.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/advetiser_slices.py new file mode 100644 index 000000000000..9a0abf1bc253 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/advetiser_slices.py @@ -0,0 +1,18 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +import json + +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +from airbyte_cdk.test.mock_http.response_builder import find_template + +ADVERTISERS_FILE = "advertisers" + + +def mock_advertisers_slices(http_mocker: HttpMocker, config: dict): + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/oauth2/advertiser/get/", + query_params={"secret": config["credentials"]["secret"], "app_id": config["credentials"]["app_id"]}, + ), + HttpResponse(body=json.dumps(find_template(ADVERTISERS_FILE, __file__)), status_code=200), + ) diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/config_builder.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/config_builder.py new file mode 100644 index 000000000000..5c32c0b9ea7d --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/config_builder.py @@ -0,0 +1,29 @@ +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. + + +from typing import Any, Dict + + +class ConfigBuilder: + def __init__(self) -> None: + self._config: Dict[str, Any] = { + "credentials": { + "auth_type": "oauth2.0", + "access_token": "access token", + "app_id": "11111111111111111111", + "secret": "secret" + }, + "start_date": "2024-01-01", + "include_deleted": False + } + + def with_include_deleted(self) -> "ConfigBuilder": + self._config["include_deleted"] = True + return self + + def with_end_date(self, date: str) -> "ConfigBuilder": + self._config["end_date"] = date + return self + + def build(self) -> Dict[str, Any]: + return self._config diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_creative_assets_music.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_creative_assets_music.py new file mode 100644 index 000000000000..492c3cae854e --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_creative_assets_music.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +import json +from unittest import TestCase + +from advetiser_slices import mock_advertisers_slices +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +from airbyte_cdk.test.mock_http.response_builder import find_template +from airbyte_protocol.models import SyncMode +from config_builder import ConfigBuilder +from source_tiktok_marketing import SourceTiktokMarketing + + +class TestCreativeAssetsMusic(TestCase): + stream_name = "creative_assets_music" + advertiser_id = "872746382648" + + def catalog(self, sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name=self.stream_name, sync_mode=sync_mode).build() + + def config(self): + return ConfigBuilder().build() + + @HttpMocker() + def test_basic_read(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/file/music/get/?page_size=100&advertiser_id=872746382648", + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + + output = read(SourceTiktokMarketing(), self.config(), self.catalog()) + assert len(output.records) == 2 diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_creative_assets_portfolios.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_creative_assets_portfolios.py new file mode 100644 index 000000000000..90ee18eb1a11 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_creative_assets_portfolios.py @@ -0,0 +1,38 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +import json +from unittest import TestCase + +from advetiser_slices import mock_advertisers_slices +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +from airbyte_cdk.test.mock_http.response_builder import find_template +from airbyte_protocol.models import SyncMode +from config_builder import ConfigBuilder +from source_tiktok_marketing import SourceTiktokMarketing + + +class TestCreativeAssetsPortfolios(TestCase): + stream_name = "creative_assets_portfolios" + advertiser_id = "872746382648" + + def catalog(self, sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name=self.stream_name, sync_mode=sync_mode).build() + + def config(self): + return ConfigBuilder().build() + + @HttpMocker() + def test_basic_read(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/creative/portfolio/list/?page_size=100&advertiser_id=872746382648", + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + + output = read(SourceTiktokMarketing(), self.config(), self.catalog()) + assert len(output.records) == 2 diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_reports_hourly.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_reports_hourly.py new file mode 100644 index 000000000000..ed9f19bde77c --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/integration/test_reports_hourly.py @@ -0,0 +1,579 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +import json +from unittest import TestCase + +from advetiser_slices import mock_advertisers_slices +from airbyte_cdk.test.catalog_builder import CatalogBuilder +from airbyte_cdk.test.entrypoint_wrapper import read +from airbyte_cdk.test.mock_http import HttpMocker, HttpRequest, HttpResponse +from airbyte_cdk.test.mock_http.response_builder import find_template +from airbyte_cdk.test.state_builder import StateBuilder +from airbyte_protocol.models import SyncMode +from config_builder import ConfigBuilder +from source_tiktok_marketing import SourceTiktokMarketing + +EMPTY_LIST_RESPONSE = {"code": 0, "message": "ok", "data": {"list": []}} + + +class TestAdsReportHourly(TestCase): + stream_name = "ads_reports_hourly" + advertiser_id = "872746382648" + cursor = "2024-01-01 10:00:00" + cursor_field = "stat_time_hour" + metrics = [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + "adgroup_id", + "ad_name", + "ad_text", + "total_purchase_value", + "total_onsite_shopping_value", + "onsite_shopping", + "vta_purchase", + "vta_conversion", + "cta_purchase", + "cta_conversion", + "total_pageview", + "complete_payment", + "value_per_complete_payment", + "total_complete_payment_rate", + "spend", + "cpc", + "cpm", + "impressions", + "clicks", + "ctr", + "reach", + "cost_per_1000_reached", + "frequency", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "average_video_play", + "average_video_play_per_user", + "video_views_p25", + "video_views_p50", + "video_views_p75", + "video_views_p100", + "profile_visits", + "likes", + "comments", + "shares", + "follows", + "clicks_on_music_disc", + "real_time_app_install", + "real_time_app_install_cost", + "app_install", + ] + + def catalog(self, sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name=self.stream_name, sync_mode=sync_mode).build() + + def config(self): + return ConfigBuilder().with_end_date("2024-01-02").build() + + def state(self): + return ( + StateBuilder() + .with_stream_state( + stream_name=self.stream_name, + state={ + "states": [ + {"partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}, "cursor": {self.cursor_field: self.cursor}} + ] + }, + ) + .build() + ) + + def mock_response(self, http_mocker: HttpMocker): + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_AD", + "dimensions": '["ad_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["start_date"], + "end_date": self.config()["start_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_AD", + "dimensions": '["ad_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["end_date"], + "end_date": self.config()["end_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(EMPTY_LIST_RESPONSE), status_code=200), + ) + + @HttpMocker() + def test_basic_read(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + self.mock_response(http_mocker) + + output = read(SourceTiktokMarketing(), self.config(), self.catalog()) + assert len(output.records) == 2 + assert output.records[0].record.data.get("ad_id") is not None + assert output.records[0].record.data.get("stat_time_hour") is not None + + @HttpMocker() + def test_read_with_state(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + self.mock_response(http_mocker) + + output = read( + source=SourceTiktokMarketing(), config=self.config(), catalog=self.catalog(sync_mode=SyncMode.incremental), state=self.state() + ) + + assert len(output.records) == 1 + assert output.state_messages[0].state.stream.stream_state.dict()["states"] == [ + {"cursor": {"stat_time_hour": self.cursor}, "partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}} + ] + + +class TestAdGroupsReportsHourly(TestCase): + stream_name = "ad_groups_reports_hourly" + advertiser_id = "872746382648" + cursor = "2024-01-01 10:00:00" + cursor_field = "stat_time_hour" + metrics = [ + "campaign_name", + "campaign_id", + "adgroup_name", + "placement_type", + "tt_app_id", + "tt_app_name", + "mobile_app_id", + "promotion_type", + "dpa_target_audience_type", + "conversion", + "cost_per_conversion", + "conversion_rate", + "real_time_conversion", + "real_time_cost_per_conversion", + "real_time_conversion_rate", + "result", + "cost_per_result", + "result_rate", + "real_time_result", + "real_time_cost_per_result", + "real_time_result_rate", + "secondary_goal_result", + "cost_per_secondary_goal_result", + "secondary_goal_result_rate", + "spend", + "cpc", + "cpm", + "impressions", + "clicks", + "ctr", + "reach", + "cost_per_1000_reached", + "frequency", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "average_video_play", + "average_video_play_per_user", + "video_views_p25", + "video_views_p50", + "video_views_p75", + "video_views_p100", + "profile_visits", + "likes", + "comments", + "shares", + "follows", + "clicks_on_music_disc", + "real_time_app_install", + "real_time_app_install_cost", + "app_install", + ] + + def catalog(self, sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name=self.stream_name, sync_mode=sync_mode).build() + + def config(self): + return ConfigBuilder().with_end_date("2024-01-02").build() + + def state(self): + return ( + StateBuilder() + .with_stream_state( + stream_name=self.stream_name, + state={ + "states": [ + {"partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}, "cursor": {self.cursor_field: self.cursor}} + ] + }, + ) + .build() + ) + + @HttpMocker() + def test_basic_read(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_ADGROUP", + "dimensions": '["adgroup_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["start_date"], + "end_date": self.config()["start_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_ADGROUP", + "dimensions": '["adgroup_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["end_date"], + "end_date": self.config()["end_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(EMPTY_LIST_RESPONSE), status_code=200), + ) + + output = read(SourceTiktokMarketing(), self.config(), self.catalog()) + assert len(output.records) == 2 + assert output.records[0].record.data.get("adgroup_id") is not None + assert output.records[0].record.data.get("stat_time_hour") is not None + + @HttpMocker() + def test_read_with_state(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_ADGROUP", + "dimensions": '["adgroup_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["start_date"], + "end_date": self.config()["start_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_ADGROUP", + "dimensions": '["adgroup_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["end_date"], + "end_date": self.config()["end_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(EMPTY_LIST_RESPONSE), status_code=200), + ) + + output = read( + source=SourceTiktokMarketing(), config=self.config(), catalog=self.catalog(sync_mode=SyncMode.incremental), state=self.state() + ) + + assert len(output.records) == 1 + assert output.state_messages[0].state.stream.stream_state.dict()["states"] == [ + {"cursor": {"stat_time_hour": self.cursor}, "partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}} + ] + + +class TestAdvertisersReportsHourly(TestCase): + stream_name = "advertisers_reports_hourly" + advertiser_id = "872746382648" + cursor = "2024-01-01 10:00:00" + cursor_field = "stat_time_hour" + metrics = [ + "spend", + "cpc", + "cpm", + "impressions", + "clicks", + "ctr", + "reach", + "cost_per_1000_reached", + "frequency", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "average_video_play", + "average_video_play_per_user", + "video_views_p25", + "video_views_p50", + "video_views_p75", + "video_views_p100", + "profile_visits", + "likes", + "comments", + "shares", + "follows", + "clicks_on_music_disc", + "real_time_app_install", + "real_time_app_install_cost", + "app_install", + ] + + def catalog(self, sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name=self.stream_name, sync_mode=sync_mode).build() + + def config(self): + return ConfigBuilder().with_end_date("2024-01-02").build() + + def state(self): + return ( + StateBuilder() + .with_stream_state( + stream_name=self.stream_name, + state={ + "states": [ + {"partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}, "cursor": {self.cursor_field: self.cursor}} + ] + }, + ) + .build() + ) + + def mock_response(self, http_mocker: HttpMocker): + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_ADVERTISER", + "dimensions": '["advertiser_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["start_date"], + "end_date": self.config()["start_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_ADVERTISER", + "dimensions": '["advertiser_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["end_date"], + "end_date": self.config()["end_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(EMPTY_LIST_RESPONSE), status_code=200), + ) + + @HttpMocker() + def test_basic_read(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + self.mock_response(http_mocker) + + output = read(SourceTiktokMarketing(), self.config(), self.catalog()) + assert len(output.records) == 2 + assert output.records[0].record.data.get("advertiser_id") is not None + assert output.records[0].record.data.get("stat_time_hour") is not None + + @HttpMocker() + def test_read_with_state(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + self.mock_response(http_mocker) + + output = read( + source=SourceTiktokMarketing(), config=self.config(), catalog=self.catalog(sync_mode=SyncMode.incremental), state=self.state() + ) + + assert len(output.records) == 1 + assert output.state_messages[0].state.stream.stream_state.dict()["states"] == [ + {"cursor": {"stat_time_hour": self.cursor}, "partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}} + ] + + +class TestCampaignsReportsHourly(TestCase): + stream_name = "campaigns_reports_hourly" + advertiser_id = "872746382648" + cursor = "2024-01-01 10:00:00" + cursor_field = "stat_time_hour" + metrics = [ + "campaign_name", + "spend", + "cpc", + "cpm", + "impressions", + "clicks", + "ctr", + "reach", + "cost_per_1000_reached", + "frequency", + "video_play_actions", + "video_watched_2s", + "video_watched_6s", + "average_video_play", + "average_video_play_per_user", + "video_views_p25", + "video_views_p50", + "video_views_p75", + "video_views_p100", + "profile_visits", + "likes", + "comments", + "shares", + "follows", + "clicks_on_music_disc", + "real_time_app_install", + "real_time_app_install_cost", + "app_install", + ] + + def catalog(self, sync_mode: SyncMode = SyncMode.full_refresh): + return CatalogBuilder().with_stream(name=self.stream_name, sync_mode=sync_mode).build() + + def config(self): + return ConfigBuilder().with_end_date("2024-01-02").build() + + def state(self): + return ( + StateBuilder() + .with_stream_state( + stream_name=self.stream_name, + state={ + "states": [ + {"partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}, "cursor": {self.cursor_field: self.cursor}} + ] + }, + ) + .build() + ) + + def mock_response(self, http_mocker: HttpMocker): + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_CAMPAIGN", + "dimensions": '["campaign_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["start_date"], + "end_date": self.config()["start_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(find_template(self.stream_name, __file__)), status_code=200), + ) + + http_mocker.get( + HttpRequest( + url=f"https://business-api.tiktok.com/open_api/v1.3/report/integrated/get/", + query_params={ + "service_type": "AUCTION", + "report_type": "BASIC", + "data_level": "AUCTION_CAMPAIGN", + "dimensions": '["campaign_id", "stat_time_hour"]', + "metrics": str(self.metrics).replace("'", '"'), + "start_date": self.config()["end_date"], + "end_date": self.config()["end_date"], + "page_size": 1000, + "advertiser_id": self.advertiser_id, + }, + ), + HttpResponse(body=json.dumps(EMPTY_LIST_RESPONSE), status_code=200), + ) + + @HttpMocker() + def test_basic_read(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + self.mock_response(http_mocker) + + output = read(SourceTiktokMarketing(), self.config(), self.catalog()) + assert len(output.records) == 2 + assert output.records[0].record.data.get("campaign_id") is not None + assert output.records[0].record.data.get("stat_time_hour") is not None + + @HttpMocker() + def test_read_with_state(self, http_mocker: HttpMocker): + mock_advertisers_slices(http_mocker, self.config()) + self.mock_response(http_mocker) + + output = read( + source=SourceTiktokMarketing(), config=self.config(), catalog=self.catalog(sync_mode=SyncMode.incremental), state=self.state() + ) + + assert len(output.records) == 1 + assert output.state_messages[0].state.stream.stream_state.dict()["states"] == [ + {"cursor": {"stat_time_hour": self.cursor}, "partition": {"advertiser_id": self.advertiser_id, "parent_slice": {}}} + ] diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/ad_groups_reports_hourly.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/ad_groups_reports_hourly.json new file mode 100644 index 000000000000..af49d8e34aa2 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/ad_groups_reports_hourly.json @@ -0,0 +1,128 @@ +{ + "code": 0, + "message": "ok", + "data": { + "list": [ + { + "dimensions": { + "stat_time_hour": "2024-01-01 09:00:00", + "adgroup_id": "11111111" + }, + "metrics": { + "shares": 0, + "real_time_result": "69", + "result": "69", + "clicks": "69", + "cpc": "0.29", + "real_time_result_rate": "1.18", + "conversion": "0", + "average_video_play_per_user": 1.64, + "cost_per_conversion": "0.00", + "cost_per_secondary_goal_result": null, + "spend": "20.00", + "likes": 36, + "profile_visits": 0, + "clicks_on_music_disc": 0, + "video_play_actions": 5173, + "secondary_goal_result": null, + "tt_app_name": "0", + "ctr": "1.18", + "promotion_type": "Website", + "video_views_p50": 214, + "cpm": "3.43", + "real_time_app_install_cost": 0, + "video_views_p75": 140, + "mobile_app_id": "0", + "cost_per_1000_reached": "4.16", + "video_watched_6s": 180, + "average_video_play": 1.52, + "adgroup_name": "Ad Group20211020010107", + "video_watched_2s": 686, + "real_time_conversion_rate": "0.00", + "video_views_p100": 92, + "placement_type": "Automatic Placement", + "conversion_rate": "0.00", + "cost_per_result": "0.290", + "real_time_cost_per_result": "0.290", + "tt_app_id": 0, + "secondary_goal_result_rate": null, + "dpa_target_audience_type": null, + "result_rate": "1.18", + "real_time_app_install": 0, + "comments": 0, + "campaign_name": "Website Traffic20211020010104", + "app_install": 0, + "real_time_cost_per_conversion": "0.00", + "impressions": "5830", + "reach": "4806", + "real_time_conversion": "0", + "follows": 0, + "video_views_p25": 513, + "frequency": "1.21", + "campaign_id": 1714125042508817 + }, + "advertiser_id": 872746382648 + }, + { + "dimensions": { + "stat_time_hour": "2024-01-01 10:00:00", + "adgroup_id": "11111111" + }, + "metrics": { + "shares": 0, + "real_time_result": "69", + "result": "69", + "clicks": "69", + "cpc": "0.29", + "real_time_result_rate": "1.18", + "conversion": "0", + "average_video_play_per_user": 1.64, + "cost_per_conversion": "0.00", + "cost_per_secondary_goal_result": null, + "spend": "20.00", + "likes": 36, + "profile_visits": 0, + "clicks_on_music_disc": 0, + "video_play_actions": 5173, + "secondary_goal_result": null, + "tt_app_name": "0", + "ctr": "1.18", + "promotion_type": "Website", + "video_views_p50": 214, + "cpm": "3.43", + "real_time_app_install_cost": 0, + "video_views_p75": 140, + "mobile_app_id": "0", + "cost_per_1000_reached": "4.16", + "video_watched_6s": 180, + "average_video_play": 1.52, + "adgroup_name": "Ad Group20211020010107", + "video_watched_2s": 686, + "real_time_conversion_rate": "0.00", + "video_views_p100": 92, + "placement_type": "Automatic Placement", + "conversion_rate": "0.00", + "cost_per_result": "0.290", + "real_time_cost_per_result": "0.290", + "tt_app_id": 0, + "secondary_goal_result_rate": null, + "dpa_target_audience_type": null, + "result_rate": "1.18", + "real_time_app_install": 0, + "comments": 0, + "campaign_name": "Website Traffic20211020010104", + "app_install": 0, + "real_time_cost_per_conversion": "0.00", + "impressions": "5830", + "reach": "4806", + "real_time_conversion": "0", + "follows": 0, + "video_views_p25": 513, + "frequency": "1.21", + "campaign_id": 1714125042508817 + }, + "advertiser_id": 872746382648 + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/ads_reports_hourly.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/ads_reports_hourly.json new file mode 100644 index 000000000000..b288c6c5d819 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/ads_reports_hourly.json @@ -0,0 +1,156 @@ +{ + "code": 0, + "message": "ok", + "data": { + "list": [ + { + "dimensions": { + "stat_time_hour": "2024-01-01 09:00:00", + "ad_id": "11111111" + }, + "metrics": { + "cpm": "3.43", + "shares": 0, + "real_time_cost_per_result": "0.290", + "video_views_p75": 140, + "follows": 0, + "comments": 0, + "mobile_app_id": "0", + "tt_app_id": 0, + "video_watched_6s": 180, + "cost_per_result": "0.290", + "average_video_play_per_user": 1.64, + "cta_purchase": "0", + "real_time_conversion": "0", + "real_time_cost_per_conversion": "0.00", + "promotion_type": "Website", + "video_views_p50": 214, + "cost_per_secondary_goal_result": null, + "ctr": "1.18", + "real_time_result_rate": "1.18", + "real_time_app_install_cost": 0, + "impressions": "5830", + "conversion": "0", + "cta_conversion": "0", + "placement_type": "Automatic Placement", + "profile_visits": 0, + "result": "69", + "cost_per_1000_reached": "4.16", + "video_views_p25": 513, + "campaign_id": 1714125042508817, + "vta_purchase": "0", + "tt_app_name": "0", + "onsite_shopping": "0", + "total_pageview": "0", + "cpc": "0.29", + "complete_payment": "0", + "dpa_target_audience_type": null, + "total_onsite_shopping_value": "0.00", + "vta_conversion": "0", + "spend": "20.00", + "real_time_result": "69", + "secondary_goal_result_rate": null, + "conversion_rate": "0.00", + "secondary_goal_result": null, + "adgroup_name": "Ad Group20211020010107", + "total_purchase_value": "0.00", + "result_rate": "1.18", + "ad_text": "Airbyte - data portabioolity platform - from anywhere to anywhere!", + "ad_name": "Optimized Version 4_202110201102_2021-10-20 11:02:00", + "likes": 36, + "video_watched_2s": 686, + "real_time_app_install": 0, + "reach": "4806", + "total_complete_payment_rate": "0.00", + "clicks": "69", + "cost_per_conversion": "0.00", + "app_install": 0, + "real_time_conversion_rate": "0.00", + "video_play_actions": 5173, + "value_per_complete_payment": "0.00", + "frequency": "1.21", + "average_video_play": 1.52, + "video_views_p100": 92, + "clicks_on_music_disc": 0, + "adgroup_id": 1714125049901106, + "campaign_name": "Website Traffic20211020010104" + }, + "advertiser_id": 872746382648 + }, + { + "dimensions": { + "stat_time_hour": "2024-01-01 10:00:00", + "ad_id": "11111111" + }, + "metrics": { + "cpm": "3.43", + "shares": 0, + "real_time_cost_per_result": "0.290", + "video_views_p75": 140, + "follows": 0, + "comments": 0, + "mobile_app_id": "0", + "tt_app_id": 0, + "video_watched_6s": 180, + "cost_per_result": "0.290", + "average_video_play_per_user": 1.64, + "cta_purchase": "0", + "real_time_conversion": "0", + "real_time_cost_per_conversion": "0.00", + "promotion_type": "Website", + "video_views_p50": 214, + "cost_per_secondary_goal_result": null, + "ctr": "1.18", + "real_time_result_rate": "1.18", + "real_time_app_install_cost": 0, + "impressions": "5830", + "conversion": "0", + "cta_conversion": "0", + "placement_type": "Automatic Placement", + "profile_visits": 0, + "result": "69", + "cost_per_1000_reached": "4.16", + "video_views_p25": 513, + "campaign_id": 1714125042508817, + "vta_purchase": "0", + "tt_app_name": "0", + "onsite_shopping": "0", + "total_pageview": "0", + "cpc": "0.29", + "complete_payment": "0", + "dpa_target_audience_type": null, + "total_onsite_shopping_value": "0.00", + "vta_conversion": "0", + "spend": "20.00", + "real_time_result": "69", + "secondary_goal_result_rate": null, + "conversion_rate": "0.00", + "secondary_goal_result": null, + "adgroup_name": "Ad Group20211020010107", + "total_purchase_value": "0.00", + "result_rate": "1.18", + "ad_text": "Airbyte - data portabioolity platform - from anywhere to anywhere!", + "ad_name": "Optimized Version 4_202110201102_2021-10-20 11:02:00", + "likes": 36, + "video_watched_2s": 686, + "real_time_app_install": 0, + "reach": "4806", + "total_complete_payment_rate": "0.00", + "clicks": "69", + "cost_per_conversion": "0.00", + "app_install": 0, + "real_time_conversion_rate": "0.00", + "video_play_actions": 5173, + "value_per_complete_payment": "0.00", + "frequency": "1.21", + "average_video_play": 1.52, + "video_views_p100": 92, + "clicks_on_music_disc": 0, + "adgroup_id": 1714125049901106, + "campaign_name": "Website Traffic20211020010104" + }, + "advertiser_id": 872746382648 + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/advertisers.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/advertisers.json new file mode 100644 index 000000000000..d4b8b453a0f9 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/advertisers.json @@ -0,0 +1,13 @@ +{ + "code": 0, + "message": "ok", + "data": { + "list": [ + { + "advertiser_id": "872746382648", + "name": "test name", + "address": "test address" + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/advertisers_reports_hourly.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/advertisers_reports_hourly.json new file mode 100644 index 000000000000..a80ea3740b06 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/advertisers_reports_hourly.json @@ -0,0 +1,84 @@ +{ + "code": 0, + "message": "ok", + "data": { + "list": [ + { + "dimensions": { + "stat_time_hour": "2024-01-01 09:00:00", + "adgroup_id": "advertiser_id" + }, + "metrics": { + "cpm": "4.18", + "impressions": "4787", + "video_watched_6s": 120, + "profile_visits": 0, + "average_video_play_per_user": 1.54, + "shares": 0, + "app_install": 0, + "clicks_on_music_disc": 0, + "likes": 18, + "cpc": "0.42", + "real_time_app_install": 0, + "average_video_play": 1.42, + "cash_spend": "20.00", + "reach": "3938", + "real_time_app_install_cost": 0, + "video_play_actions": 4253, + "video_views_p75": 100, + "video_views_p100": 70, + "video_watched_2s": 471, + "comments": 0, + "cost_per_1000_reached": "5.08", + "follows": 0, + "spend": "20.00", + "ctr": "1.00", + "video_views_p25": 328, + "frequency": "1.22", + "video_views_p50": 144, + "voucher_spend": "0.00", + "clicks": "48" + }, + "advertiser_id": 872746382648 + }, + { + "dimensions": { + "stat_time_hour": "2024-01-01 10:00:00", + "advertiser_id": "11111111" + }, + "metrics": { + "cpm": "4.18", + "impressions": "4787", + "video_watched_6s": 120, + "profile_visits": 0, + "average_video_play_per_user": 1.54, + "shares": 0, + "app_install": 0, + "clicks_on_music_disc": 0, + "likes": 18, + "cpc": "0.42", + "real_time_app_install": 0, + "average_video_play": 1.42, + "cash_spend": "20.00", + "reach": "3938", + "real_time_app_install_cost": 0, + "video_play_actions": 4253, + "video_views_p75": 100, + "video_views_p100": 70, + "video_watched_2s": 471, + "comments": 0, + "cost_per_1000_reached": "5.08", + "follows": 0, + "spend": "20.00", + "ctr": "1.00", + "video_views_p25": 328, + "frequency": "1.22", + "video_views_p50": 144, + "voucher_spend": "0.00", + "clicks": "48" + }, + "advertiser_id": 872746382648 + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/campaigns_reports_hourly.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/campaigns_reports_hourly.json new file mode 100644 index 000000000000..2a5ac5965daa --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/campaigns_reports_hourly.json @@ -0,0 +1,82 @@ +{ + "code": 0, + "message": "ok", + "data": { + "list": [ + { + "dimensions": { + "stat_time_hour": "2024-01-01 09:00:00", + "campaign_id": "advertiser_id" + }, + "metrics": { + "video_watched_2s": 493, + "profile_visits": 0, + "ctr": "1.09", + "likes": 18, + "video_play_actions": 4179, + "shares": 0, + "cpc": "0.39", + "cpm": "4.26", + "video_views_p75": 108, + "video_views_p100": 76, + "real_time_app_install_cost": 0, + "video_views_p25": 355, + "spend": "20.00", + "video_watched_6s": 132, + "reach": "3822", + "impressions": "4696", + "real_time_app_install": 0, + "campaign_name": "Website Traffic20211020010104", + "app_install": 0, + "video_views_p50": 164, + "comments": 0, + "follows": 0, + "cost_per_1000_reached": "5.23", + "average_video_play": 1.48, + "average_video_play_per_user": 1.61, + "clicks": "51", + "clicks_on_music_disc": 0, + "frequency": "1.23" + }, + "advertiser_id": 872746382648 + }, + { + "dimensions": { + "stat_time_hour": "2024-01-01 10:00:00", + "campaign_id": "11111111" + }, + "metrics": { + "video_watched_2s": 493, + "profile_visits": 0, + "ctr": "1.09", + "likes": 18, + "video_play_actions": 4179, + "shares": 0, + "cpc": "0.39", + "cpm": "4.26", + "video_views_p75": 108, + "video_views_p100": 76, + "real_time_app_install_cost": 0, + "video_views_p25": 355, + "spend": "20.00", + "video_watched_6s": 132, + "reach": "3822", + "impressions": "4696", + "real_time_app_install": 0, + "campaign_name": "Website Traffic20211020010104", + "app_install": 0, + "video_views_p50": 164, + "comments": 0, + "follows": 0, + "cost_per_1000_reached": "5.23", + "average_video_play": 1.48, + "average_video_play_per_user": 1.61, + "clicks": "51", + "clicks_on_music_disc": 0, + "frequency": "1.23" + }, + "advertiser_id": 872746382648 + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/creative_assets_music.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/creative_assets_music.json new file mode 100644 index 000000000000..5a41eb37513a --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/creative_assets_music.json @@ -0,0 +1,42 @@ +{ + "code": 0, + "message": "ok", + "data": { + "musics": [ + { + "music_id": "111111111111111111", + "material_id": "111111111111111111", + "sources": [{ "type": "main page" }], + "author": "test author", + "liked": true, + "cover_url": "https://cover.com", + "url": "https://url.com", + "duration": 4.34, + "style": "rock", + "signature": "signature", + "name": "test name", + "file_name": "test file_name", + "copyright": "copyright", + "create_time": "2024-01-01 12:12:23", + "modify_time": "2024-01-03 12:12:23" + }, + { + "music_id": "22222222222", + "material_id": "22222222222222222", + "sources": [{ "type": "main page" }], + "author": "test author", + "liked": true, + "cover_url": "https://cover.com", + "url": "https://url.com", + "duration": 4.34, + "style": "rock", + "signature": "signature", + "name": "test name", + "file_name": "test file_name", + "copyright": "copyright", + "create_time": "2024-02-01 12:12:23", + "modify_time": "2024-02-03 12:12:23" + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/creative_assets_portfolios.json b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/creative_assets_portfolios.json new file mode 100644 index 000000000000..192a20fdb12d --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/resource/http/response/creative_assets_portfolios.json @@ -0,0 +1,18 @@ +{ + "code": 0, + "message": "ok", + "data": { + "creative_portfolios": [ + { + "creative_portfolio_id": "111111111111111111", + "creative_portfolio_type": "portfolio", + "creative_portfolio_preview_url": "https://url1.com" + }, + { + "creative_portfolio_id": "222222222222222222", + "creative_portfolio_type": "portfolio", + "creative_portfolio_preview_url": "https://url2.com" + } + ] + } +} diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/streams_test.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/streams_test.py deleted file mode 100644 index b77b35b3a528..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/streams_test.py +++ /dev/null @@ -1,296 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -from decimal import Decimal -from unittest.mock import MagicMock, PropertyMock, patch - -import pendulum -import pytest -import requests -from source_tiktok_marketing.source import get_report_stream -from source_tiktok_marketing.streams import ( - AdGroupsReports, - Ads, - AdsAudienceReports, - AdsReports, - Advertisers, - AdvertisersAudienceReports, - AdvertisersReports, - BasicReports, - CampaignsReports, - Daily, - FullRefreshTiktokStream, - Hourly, - Lifetime, - ReportGranularity, -) - -START_DATE = "2020-01-01" -END_DATE = "2020-03-01" -CONFIG = { - "access_token": "access_token", - "secret": "secret", - "authenticator": None, - "start_date": START_DATE, - "end_date": END_DATE, - "app_id": 1234, - "advertiser_id": 0, - "include_deleted": True, -} -CONFIG_SANDBOX = { - "access_token": "access_token", - "secret": "secret", - "authenticator": None, - "start_date": START_DATE, - "end_date": END_DATE, - "app_id": 1234, - "advertiser_id": 2000, -} -ADV_IDS = [{"advertiser_id": 1}, {"advertiser_id": 2}] - - -@pytest.fixture(scope="module") -def pendulum_now_mock(): - with patch.object(pendulum, "now", return_value=pendulum.parse(END_DATE)): - yield - - -@pytest.fixture(name="advertiser_ids") -def advertiser_ids_fixture(): - with patch("source_tiktok_marketing.streams.AdvertiserIds") as advertiser_ids_stream: - advertiser_ids_stream().read_records = MagicMock(return_value=ADV_IDS) - yield - - -@pytest.mark.parametrize( - "granularity,intervals_len", - [ - (ReportGranularity.LIFETIME, 1), - (ReportGranularity.DAY, 3), - (ReportGranularity.HOUR, 61), - ], -) -def test_get_time_interval(pendulum_now_mock, granularity, intervals_len): - intervals = BasicReports._get_time_interval(start_date="2020-01-01", ending_date="2020-03-01", granularity=granularity) - assert len(list(intervals)) == intervals_len - - -@patch.object(pendulum, "now", return_value=pendulum.parse("2018-12-25")) -def test_get_time_interval_past(pendulum_now_mock_past): - intervals = BasicReports._get_time_interval(start_date="2020-01-01", ending_date="2020-01-01", granularity=ReportGranularity.DAY) - assert len(list(intervals)) == 1 - - -@patch("source_tiktok_marketing.streams.AdvertiserIds.read_records", MagicMock(return_value=[{"advertiser_id": i} for i in range(354)])) -def test_stream_slices_advertisers(): - slices = Advertisers(**CONFIG).stream_slices() - assert len(list(slices)) == 4 # math.ceil(354 / 100) - - -@pytest.mark.parametrize( - "config_name, slices_expected", - [ - (CONFIG, ADV_IDS), - (CONFIG_SANDBOX, [{"advertiser_id": 2000}]), - ], -) -def test_stream_slices_basic_sandbox(advertiser_ids, config_name, slices_expected): - slices = Ads(**config_name).stream_slices() - assert list(slices) == slices_expected - - -@pytest.mark.parametrize( - "granularity, slices_expected", - [ - ( - Lifetime, - [ - {"advertiser_id": 1, "end_date": END_DATE, "start_date": START_DATE}, - {"advertiser_id": 2, "end_date": END_DATE, "start_date": START_DATE}, - ], - ), - ( - Daily, - [ - {"advertiser_id": 1, "end_date": "2020-01-30", "start_date": "2020-01-01"}, - {"advertiser_id": 1, "end_date": "2020-02-29", "start_date": "2020-01-31"}, - {"advertiser_id": 1, "end_date": "2020-03-01", "start_date": "2020-03-01"}, - {"advertiser_id": 2, "end_date": "2020-01-30", "start_date": "2020-01-01"}, - {"advertiser_id": 2, "end_date": "2020-02-29", "start_date": "2020-01-31"}, - {"advertiser_id": 2, "end_date": "2020-03-01", "start_date": "2020-03-01"}, - ], - ), - ], -) -def test_stream_slices_report(advertiser_ids, granularity, slices_expected, pendulum_now_mock): - slices = get_report_stream(AdsReports, granularity)(**CONFIG).stream_slices() - assert list(slices) == slices_expected - - -@pytest.mark.parametrize( - "stream, metrics_number", - [ - (AdsReports, 65), - (AdGroupsReports, 51), - (AdvertisersReports, 29), - (CampaignsReports, 28), - (AdvertisersAudienceReports, 6), - (AdsAudienceReports, 30), - ], -) -def test_basic_reports_get_metrics_day(stream, metrics_number): - metrics = get_report_stream(stream, Daily)(**CONFIG)._get_metrics() - assert len(metrics) == metrics_number - - -@pytest.mark.parametrize( - "stream, metrics_number", - [ - (AdsReports, 65), - (AdGroupsReports, 51), - (AdvertisersReports, 27), - (CampaignsReports, 28), - (AdvertisersAudienceReports, 6), - ], -) -def test_basic_reports_get_metrics_lifetime(stream, metrics_number): - metrics = get_report_stream(stream, Lifetime)(**CONFIG)._get_metrics() - assert len(metrics) == metrics_number - - -@pytest.mark.parametrize( - "stream, dimensions_expected", - [ - (AdsReports, ["ad_id"]), - (AdGroupsReports, ["adgroup_id"]), - (AdvertisersReports, ["advertiser_id"]), - (CampaignsReports, ["campaign_id"]), - (AdvertisersAudienceReports, ["advertiser_id", "gender", "age"]), - ], -) -def test_basic_reports_get_reporting_dimensions_lifetime(stream, dimensions_expected): - dimensions = get_report_stream(stream, Lifetime)(**CONFIG)._get_reporting_dimensions() - assert dimensions == dimensions_expected - - -@pytest.mark.parametrize( - "stream, dimensions_expected", - [ - (AdsReports, ["ad_id", "stat_time_day"]), - (AdGroupsReports, ["adgroup_id", "stat_time_day"]), - (AdvertisersReports, ["advertiser_id", "stat_time_day"]), - (CampaignsReports, ["campaign_id", "stat_time_day"]), - (AdvertisersAudienceReports, ["advertiser_id", "stat_time_day", "gender", "age"]), - ], -) -def test_basic_reports_get_reporting_dimensions_day(stream, dimensions_expected): - dimensions = get_report_stream(stream, Daily)(**CONFIG)._get_reporting_dimensions() - assert dimensions == dimensions_expected - - -@pytest.mark.parametrize( - "granularity, cursor_field_expected", - [ - (Daily, "stat_time_day"), - (Hourly, "stat_time_hour"), - (Lifetime, []), - ], -) -def test_basic_reports_cursor_field(granularity, cursor_field_expected): - ads_reports = get_report_stream(AdsReports, granularity)(**CONFIG) - cursor_field = ads_reports.cursor_field - assert cursor_field == cursor_field_expected - - -@pytest.mark.parametrize( - "granularity, cursor_field_expected", - [ - (Daily, ["dimensions", "stat_time_day"]), - (Hourly, ["dimensions", "stat_time_hour"]), - (Lifetime, ["dimensions", "stat_time_day"]), - ], -) -def test_basic_reports_deprecated_cursor_field(granularity, cursor_field_expected): - ads_reports = get_report_stream(AdsReports, granularity)(**CONFIG) - deprecated_cursor_field = ads_reports.deprecated_cursor_field - assert deprecated_cursor_field == cursor_field_expected - - -def test_request_params(): - stream_slice = {"advertiser_id": 1, "start_date": "2020", "end_date": "2021"} - params = get_report_stream(AdvertisersAudienceReports, Daily)(**CONFIG).request_params(stream_slice=stream_slice) - assert params == { - "advertiser_id": 1, - "data_level": "AUCTION_ADVERTISER", - "dimensions": '["advertiser_id", "stat_time_day", "gender", "age"]', - "end_date": "2021", - "metrics": '["spend", "cpc", "cpm", "impressions", "clicks", "ctr"]', - "filters": '[{"filter_value": ["STATUS_ALL"], "field_name": "ad_status", "filter_type": "IN"}, {"filter_value": ["STATUS_ALL"], "field_name": "campaign_status", "filter_type": "IN"}, {"filter_value": ["STATUS_ALL"], "field_name": "adgroup_status", "filter_type": "IN"}]', - "page_size": 1000, - "report_type": "AUDIENCE", - "service_type": "AUCTION", - "start_date": "2020", - } - - -def test_get_updated_state(): - with patch.object(Ads, "is_finished", new_callable=PropertyMock) as is_finished: - - ads = Ads(**CONFIG_SANDBOX) - - # initial state. - state = {} - - # state should be empty while stream is reading records - ads.max_cursor_date = "2020-01-08 00:00:00" - is_finished.return_value = False - state1 = ads.get_updated_state(current_stream_state=state, latest_record={}) - assert state1 == {"modify_time": ""} - - # state should be updated only when all records have been read (is_finished = True) - is_finished.return_value = True - state2 = ads.get_updated_state(current_stream_state=state, latest_record={}) - # state2_modify_time is JsonUpdatedState object - state2_modify_time = state2["modify_time"] - assert state2_modify_time.dict() == "2020-01-08 00:00:00" - - -def test_get_updated_state_no_cursor_field(): - """ - Some full_refresh streams (which don't define a cursor) inherit the get_updated_state() method from an incremental - stream. This test verifies that the stream does not attempt to extract the cursor value from the latest record - """ - ads_reports = AdsReports(**CONFIG_SANDBOX) - state1 = ads_reports.get_updated_state(current_stream_state={}, latest_record={}) - assert state1 == {} - - -@pytest.mark.parametrize( - "value, expected", - [ - (["str1", "str2", "str3"], '["str1", "str2", "str3"]'), - ([1, 2, 3], "[1, 2, 3]"), - ], -) -def test_convert_array_param(value, expected): - stream = Advertisers("2021-01-01", "2021-01-02") - test = stream.convert_array_param(value) - assert test == expected - - -def test_no_next_page_token(requests_mock): - stream = Advertisers("2021-01-01", "2021-01-02") - url = stream.url_base + stream.path() - requests_mock.get(url, json={"data": {"page_info": {}}}) - test_response = requests.get(url) - assert stream.next_page_token(test_response) is None - - -@pytest.mark.parametrize( - ("original_value", "expected_value"), - (("-", None), (26.10, Decimal(26.10)), ("some_str", "some_str")), -) -def test_transform_function(original_value, expected_value): - field_schema = {} - assert FullRefreshTiktokStream.transform_function(original_value, field_schema) == expected_value diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/test_components.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/test_components.py new file mode 100644 index 000000000000..1825fa49d955 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/test_components.py @@ -0,0 +1,216 @@ +# Copyright (c) 2024 Airbyte, Inc., all rights reserved. + +from unittest.mock import MagicMock + +import pytest +from airbyte_cdk.sources.declarative.datetime.min_max_datetime import MinMaxDatetime +from airbyte_cdk.sources.declarative.partition_routers.substream_partition_router import ParentStreamConfig +from airbyte_cdk.sources.declarative.types import StreamSlice +from source_tiktok_marketing import SourceTiktokMarketing +from source_tiktok_marketing.components.advertiser_ids_partition_router import ( + MultipleAdvertiserIdsPerPartition, + SingleAdvertiserIdPerPartition, +) +from source_tiktok_marketing.components.hourly_datetime_based_cursor import HourlyDatetimeBasedCursor +from source_tiktok_marketing.components.semi_incremental_record_filter import PerPartitionRecordFilter +from source_tiktok_marketing.components.transformations import TransformEmptyMetrics + + +@pytest.mark.parametrize( + "config, expected", + [ + ({"credentials": {"advertiser_id": "11111111111"}}, "11111111111"), + ({"environment": {"advertiser_id": "2222222222"}}, "2222222222"), + ({"credentials": {"access_token": "access_token"}}, None) + ], +) +def test_get_partition_value_from_config(config, expected): + router = MultipleAdvertiserIdsPerPartition( + parent_stream_configs=[MagicMock()], + config=config, + parameters={ + "path_in_config": [["credentials", "advertiser_id"], ["environment", "advertiser_id"]], "partition_field": "advertiser_id" + } + ) + actual = router.get_partition_value_from_config() + assert actual == expected + + +@pytest.mark.parametrize( + "config, expected, json_data", + [ + ({"credentials": {"auth_type": "oauth2.0", "advertiser_id": "11111111111"}}, + [{"advertiser_ids": '["11111111111"]', "parent_slice": {}}], None), + ({"environment": {"advertiser_id": "2222222222"}}, [{"advertiser_ids": '["2222222222"]', "parent_slice": {}}], None), + ( + {"credentials": {"auth_type": "oauth2.0", "access_token": "access_token"}}, + [{"advertiser_ids": '["11111111", "22222222"]', "parent_slice": {}}], + {"code": 0, "message": "ok", "data": + {"list": [{"advertiser_id": "11111111", "advertiser_name": "name"}, + {"advertiser_id": "22222222", "advertiser_name": "name"}]} + } + ) + ], +) +def test_stream_slices_multiple(config, expected, requests_mock, json_data): + advertiser_ids_stream = [s for s in SourceTiktokMarketing().streams(config=config) if s.name == "advertiser_ids"] + advertiser_ids_stream = advertiser_ids_stream[0] if advertiser_ids_stream else MagicMock() + + router = MultipleAdvertiserIdsPerPartition( + parent_stream_configs=[ParentStreamConfig( + partition_field="advertiser_ids", + config=config, + parent_key="advertiser_id", + stream=advertiser_ids_stream, + parameters={} + )], + config=config, + parameters={ + "path_in_config": [["credentials", "advertiser_id"], ["environment", "advertiser_id"]], "partition_field": "advertiser_ids" + } + ) + if json_data: + requests_mock.get( + "https://business-api.tiktok.com/open_api/v1.3/oauth2/advertiser/get/", + json=json_data + ) + actual = list(router.stream_slices()) + assert actual == expected + + +@pytest.mark.parametrize( + "config, expected, json_data", + [ + ({"credentials": {"auth_type": "oauth2.0", "advertiser_id": "11111111111"}}, [{"advertiser_id": "11111111111", "parent_slice": {}}], + None), + ({"environment": {"advertiser_id": "2222222222"}}, [{"advertiser_id": "2222222222", "parent_slice": {}}], None), + ( + {"credentials": {"auth_type": "oauth2.0", "access_token": "access_token"}}, + [{"advertiser_id": "11111111", "parent_slice": {}}, + {"advertiser_id": "22222222", "parent_slice": {}}], + {"code": 0, "message": "ok", + "data": {"list": [ + {"advertiser_id": "11111111", "advertiser_name": "name"}, + {"advertiser_id": "22222222", "advertiser_name": "name"}]} + } + ) + ], +) +def test_stream_slices_single(config, expected, requests_mock, json_data): + advertiser_ids_stream = [s for s in SourceTiktokMarketing().streams(config=config) if s.name == "advertiser_ids"] + advertiser_ids_stream = advertiser_ids_stream[0] if advertiser_ids_stream else MagicMock() + + router = SingleAdvertiserIdPerPartition( + parent_stream_configs=[ParentStreamConfig( + partition_field="advertiser_id", + config=config, + parent_key="advertiser_id", + stream=advertiser_ids_stream, + parameters={} + )], + config=config, + parameters={ + "path_in_config": [["credentials", "advertiser_id"], ["environment", "advertiser_id"]], "partition_field": "advertiser_id" + } + ) + if json_data: + requests_mock.get( + "https://business-api.tiktok.com/open_api/v1.3/oauth2/advertiser/get/", + json=json_data + ) + actual = list(router.stream_slices()) + assert actual == expected + + +@pytest.mark.parametrize( + "records, state, slice, expected", + [ + ( + [{"id": 1, "start_time": "2024-01-01"}, {"id": 2, "start_time": "2024-01-01"}], + {}, + {}, + [{"id": 1, "start_time": "2024-01-01"}, {"id": 2, "start_time": "2024-01-01"}] + ), + ( + [{"advertiser_id": 1, "start_time": "2022-01-01"}, {"advertiser_id": 1, "start_time": "2024-01-02"}], + {"states": [{"partition": {"advertiser_id": 1, "parent_slice": {}}, "cursor": {"start_time": "2023-12-31"}}]}, + {"advertiser_id": 1}, + [{"advertiser_id": 1, "start_time": "2024-01-02"}] + ), + ( + [{"advertiser_id": 2, "start_time": "2022-01-01"}, {"advertiser_id": 2, "start_time": "2024-01-02"}], + {"states": [{"partition": {"advertiser_id": 1, "parent_slice": {}}, "cursor": {"start_time": "2023-12-31"}}]}, + {"advertiser_id": 2}, + [{"advertiser_id": 2, "start_time": "2022-01-01"}, {"advertiser_id": 2, "start_time": "2024-01-02"}], + ), + ], +) +def test_record_filter(records, state, slice, expected): + config = {"credentials": {"auth_type": "oauth2.0", "advertiser_id": "11111111111"}} + record_filter = PerPartitionRecordFilter( + config=config, + parameters={"partition_field": "advertiser_id"}, + condition="{{ record['start_time'] >= stream_state.get('start_time', config.get('start_date', '')) }}" + ) + filtered_records = list(record_filter.filter_records( + records=records, + stream_state=state, + stream_slice=StreamSlice(partition=slice, cursor_slice={}) + )) + assert filtered_records == expected + + +def test_hourly_datetime_based_cursor(): + config = {"credentials": {"auth_type": "oauth2.0", "advertiser_id": "11111111111"}, "start_date": "2022-01-01", "end_date": "2022-01-02"} + + cursor = HourlyDatetimeBasedCursor( + start_datetime=MinMaxDatetime(datetime="{{ config.get('start_date', '2016-09-01') }}", datetime_format="%Y-%m-%d", parameters={}), + end_datetime=MinMaxDatetime(datetime="{{ config.get('end_date', today_utc()) }}", datetime_format="%Y-%m-%d", parameters={}), + step="P1D", + cursor_granularity="PT1H", + config=config, + cursor_field="stat_time_hour", + datetime_format="%Y-%m-%d", + cursor_datetime_formats=["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%SZ"], + parameters={} + ) + cursor._cursor = "2022-01-01 00:00:00" + partition_daterange = list(cursor.stream_slices()) + assert partition_daterange == [ + {"start_time": "2022-01-01", "end_time": "2022-01-01"}, + {"start_time": "2022-01-02", "end_time": "2022-01-02"} + ] + + cursor._cursor = "2022-01-01 10:00:00" + partition_daterange = list(cursor.stream_slices()) + assert partition_daterange == [ + {"start_time": "2022-01-01", "end_time": "2022-01-01"}, + {"start_time": "2022-01-02", "end_time": "2022-01-02"} + ] + + +@pytest.mark.parametrize( + "record, expected", + [ + ( + {"metrics": {"metric_1": "not empty", "metric_2": "-"}}, + {"metrics": {"metric_1": "not empty", "metric_2": None}} + ), + ( + {"metrics": {"metric_1": "not empty", "metric_2": "not empty"}}, + {"metrics": {"metric_1": "not empty", "metric_2": "not empty"}} + ), + ( + {"dimensions": {"dimension_1": "not empty", "dimension_2": "not empty"}}, + {"dimensions": {"dimension_1": "not empty", "dimension_2": "not empty"}} + ), + ( + {}, + {} + ), + ], +) +def test_transform_empty_metrics(record, expected): + transformer = TransformEmptyMetrics() + actual_record = transformer.transform(record) + assert actual_record == expected diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/test_source.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/test_source.py new file mode 100644 index 000000000000..c573988deef7 --- /dev/null +++ b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/test_source.py @@ -0,0 +1,65 @@ +# +# Copyright (c) 2023 Airbyte, Inc., all rights reserved. +# + +import json +from unittest.mock import MagicMock + +import pytest +from airbyte_cdk.models import ConnectorSpecification +from source_tiktok_marketing import SourceTiktokMarketing + + +@pytest.mark.parametrize( + "config, stream_len", + [ + ({"access_token": "token", "environment": {"app_id": "1111", "secret": "secret"}, "start_date": "2021-04-01"}, 36), + ({"access_token": "token", "start_date": "2021-01-01", "environment": {"advertiser_id": "1111"}}, 28), + ({"access_token": "token", "environment": {"app_id": "1111", "secret": "secret"}, "start_date": "2021-04-01", "report_granularity": "LIFETIME"}, 15), + ({"access_token": "token", "environment": {"app_id": "1111", "secret": "secret"}, "start_date": "2021-04-01", "report_granularity": "DAY"}, 27), + ], +) +def test_source_streams(config, stream_len): + streams = SourceTiktokMarketing().streams(config=config) + assert len(streams) == stream_len + + +def test_source_spec(): + spec = SourceTiktokMarketing().spec(logger=None) + assert isinstance(spec, ConnectorSpecification) + + +@pytest.fixture(name="config") +def config_fixture(): + config = { + "account_id": 123, + "access_token": "TOKEN", + "start_date": "2019-10-10T00:00:00", + "end_date": "2020-10-10T00:00:00", + } + return config + + +def test_source_check_connection_ok(config, requests_mock): + requests_mock.get( + "https://business-api.tiktok.com/open_api/v1.3/oauth2/advertiser/get/", + json={"code": 0, "message": "ok", "data": {"list": [{"advertiser_id": "917429327", "advertiser_name": "name"}, ]}} + ) + requests_mock.get( + "https://business-api.tiktok.com/open_api/v1.3/advertiser/info/?page_size=100&advertiser_ids=%5B%22917429327%22%5D", + json={"code": 0, "message": "ok", "data": {"list": [{"advertiser_id": "917429327", "advertiser_name": "name"}, ]}} + ) + logger_mock = MagicMock() + assert SourceTiktokMarketing().check_connection(logger_mock, config) == (True, None) + + +def test_source_check_connection_failed(config, requests_mock): + requests_mock.get( + "https://business-api.tiktok.com/open_api/v1.3/oauth2/advertiser/get/", + json={"code": 40105, "message": "Access token is incorrect or has been revoked."} + ) + logger_mock = MagicMock() + assert SourceTiktokMarketing().check_connection(logger_mock, config) == ( + False, "Unable to connect to stream advertisers - Access token is incorrect or has been revoked." + ) + diff --git a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/unit_test.py b/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/unit_test.py deleted file mode 100644 index 6377e8eae84f..000000000000 --- a/airbyte-integrations/connectors/source-tiktok-marketing/unit_tests/unit_test.py +++ /dev/null @@ -1,198 +0,0 @@ -# -# Copyright (c) 2023 Airbyte, Inc., all rights reserved. -# - -import json -import random -from typing import Any, Dict, Iterable, List, Mapping, Tuple -from unittest.mock import patch - -import pendulum -import pytest -import requests_mock -import timeout_decorator -from airbyte_cdk.models import ConnectorSpecification -from airbyte_cdk.sources.streams.http.exceptions import UserDefinedBackoffException -from source_tiktok_marketing import SourceTiktokMarketing -from source_tiktok_marketing.streams import Ads, Advertisers, JsonUpdatedState - -SANDBOX_CONFIG_FILE = "secrets/sandbox_config.json" -PROD_CONFIG_FILE = "secrets/prod_config.json" - - -@pytest.fixture(scope="module") -def prepared_sandbox_args(): - """Generates streams settings from a file for sandbox""" - with open(SANDBOX_CONFIG_FILE, "r") as f: - return SourceTiktokMarketing._prepare_stream_args(json.loads(f.read())) - - -@pytest.fixture(scope="module") -def prepared_prod_args(): - """Generates streams settings from a file for production""" - with open(PROD_CONFIG_FILE, "r") as f: - return SourceTiktokMarketing._prepare_stream_args(json.loads(f.read())) - - -@timeout_decorator.timeout(20) -@pytest.mark.parametrize("error_code", (40100, 50002)) -def test_backoff(prepared_sandbox_args, error_code): - """TiktokMarketing sends the header 'Retry-After' about needed delay. - All streams have to handle it""" - stream = Advertisers(**prepared_sandbox_args) - with requests_mock.Mocker() as m: - url = stream.url_base + stream.path() - m.get(url, text=json.dumps({"code": error_code})) - with pytest.raises(UserDefinedBackoffException): - list(stream.read_records(sync_mode=None)) - - -def generate_pages(items: List[Mapping[str, Any]], page_size: int, last_empty: bool = False) -> Iterable[Tuple[int, Dict]]: - pages = [] - for i in range(0, len(items), page_size): - pages.append(items[i : i + page_size]) - if last_empty: - pages.append([]) - total_number = len(items) - for page_number, page_items in enumerate(pages, start=1): - yield ( - page_number, - { - "message": "OK", - "code": 0, - "request_id": "unique_request_id", - "data": { - "page_info": {"total_number": total_number, "page": page_number, "page_size": page_size, "total_page": len(page_items)}, - "list": page_items, - }, - }, - ) - - -def random_integer(max_value: int = 1634125471, min_value: int = 1) -> int: - return random.randint(min_value, max_value) - - -def unixtime2str(unix_time: int) -> str: - "Converts unix time to string" - return pendulum.from_timestamp(unix_time).strftime("%Y-%m-%d %H:%M:%S") - - -def test_random_items(prepared_prod_args): - stream = Ads(**prepared_prod_args) - advertiser_count = 100 - test_advertiser_ids = set([str(random_integer()) for _ in range(advertiser_count)]) - advertiser_count = len(test_advertiser_ids) - page_size = 100 - with requests_mock.Mocker() as m: - # mock for advertisers' list - advertisers = [{"advertiser_id": i, "advertiser_name": str(i)} for i in test_advertiser_ids] - for _, page_response in generate_pages(items=advertisers, page_size=advertiser_count): - m.register_uri("GET", "/open_api/v1.3/oauth2/advertiser/get/", json=page_response) - stream = Ads(**prepared_prod_args) - stream.page_size = page_size - stream.get_advertiser_ids() - assert not set(test_advertiser_ids).symmetric_difference(stream._advertiser_ids), "stream found not all advertiser IDs" - - current_state = None - max_updated_value = None - for stream_slice in stream.stream_slices(): - advertiser_id = stream_slice["advertiser_id"] - test_ad_ids = [random_integer() for _ in range(random_integer(max_value=999))] - ad_items = [] - for ad_id in test_ad_ids: - create_time = random_integer(min_value=1507901660) - ad_items.append( - { - "create_time": unixtime2str(create_time), - "modify_time": unixtime2str(create_time + 60), - "advertiser_id": advertiser_id, - "ad_id": ad_id, - } - ) - if not max_updated_value or max_updated_value < ad_items[-1][stream.cursor_field]: - max_updated_value = ad_items[-1][stream.cursor_field] - - # mock for ads - for page, page_response in generate_pages(items=ad_items, page_size=page_size, last_empty=True): - uri = f"/open_api/v1.3/ad/get/?page_size={page_size}&advertiser_id={advertiser_id}" - if page != 1: - uri += f"&page={page}" - m.register_uri("GET", uri, complete_qs=True, json=page_response) - - for record in stream.read_records(sync_mode=None, stream_slice=stream_slice): - current_state = stream.get_updated_state(current_state, record) - assert isinstance(current_state[stream.cursor_field], JsonUpdatedState), "state should be an JsonUpdatedState object" - if advertisers[-1]["advertiser_id"] != advertiser_id: - assert ( - current_state[stream.cursor_field].dict() == "" - ), "max updated cursor value should be returned for last slice only" - assert len(stream._advertiser_ids) == 0, "all advertisers should be popped" - assert current_state[stream.cursor_field].dict() == max_updated_value - - -@pytest.mark.parametrize( - "config, stream_len", - [ - (PROD_CONFIG_FILE, 36), - (SANDBOX_CONFIG_FILE, 28), - ], -) -def test_source_streams(config, stream_len): - with open(config) as f: - config = json.load(f) - streams = SourceTiktokMarketing().streams(config=config) - assert len(streams) == stream_len - - -def test_source_spec(): - spec = SourceTiktokMarketing().spec(logger=None) - assert isinstance(spec, ConnectorSpecification) - - -@pytest.fixture(name="config") -def config_fixture(): - config = { - "account_id": 123, - "access_token": "TOKEN", - "start_date": "2019-10-10T00:00:00", - "end_date": "2020-10-10T00:00:00", - } - return config - - -@pytest.fixture(name="logger_mock") -def logger_mock_fixture(): - return patch("source_tiktok_marketing.source.logger") - - -def test_source_check_connection_ok(config, logger_mock): - with patch.object(Advertisers, "stream_slices"): - with patch.object(Advertisers, "read_records", return_value=iter([1])): - assert SourceTiktokMarketing().check_connection(logger_mock, config=config) == (True, None) - - -def test_source_check_connection_failed(config, logger_mock): - with patch.object(Advertisers, "read_records", return_value=0): - assert SourceTiktokMarketing().check_connection(logger_mock, config=config)[0] is False - - -@pytest.mark.parametrize( - "config_file", - ["integration_tests/invalid_config_oauth.json", "integration_tests/invalid_config_access_token.json", "secrets/config.json"], -) -def test_source_prepare_stream_args(config_file): - with open(config_file) as f: - config = json.load(f) - args = SourceTiktokMarketing._prepare_stream_args(config) - assert "authenticator" in args - - -def test_minimum_start_date(config, caplog): - config["start_date"] = "2000-01-01" - source = SourceTiktokMarketing() - streams = source.streams(config) - - for stream in streams: - assert stream._start_time == "2012-01-01 00:00:00" - assert "The start date is too far in the past. Setting it to 2012-01-01" in caplog.text diff --git a/docs/integrations/sources/tiktok-marketing-migrations.md b/docs/integrations/sources/tiktok-marketing-migrations.md new file mode 100644 index 000000000000..930fadf2c016 --- /dev/null +++ b/docs/integrations/sources/tiktok-marketing-migrations.md @@ -0,0 +1,74 @@ +# TikTok Marketing Migration Guide + +## Upgrading to 4.0.0 + +We're continuously striving to enhance the quality and reliability of our connectors at Airbyte. As part of our commitment to delivering exceptional service, we are transitioning source TikTok Marketing from the Python Connector Development Kit (CDK) to our innovative low-code framework. This is part of a strategic move to streamline many processes across connectors, bolstering maintainability and freeing us to focus more of our efforts on improving the performance and features of our evolving platform and growing catalog. However, due to differences between the Python and low-code CDKs, this migration constitutes a breaking change. + +We’ve evolved and standardized how state is managed for incremental streams that are nested within a parent stream. This change impacts how individual states are tracked and stored for each partition, using a more structured approach to ensure the most granular and flexible state management. + +This change will affect the following streams: +- `ad_group_audience_reports_by_country_daily` +- `ad_group_audience_reports_by_platform_daily` +- `ad_group_audience_reports_daily` +- `ad_groups` +- `ad_groups_reports_daily` +- `ad_groups_reports_hourly` +- `ads` +- `ads_audience_reports_by_country_daily` +- `ads_audience_reports_by_platform_daily` +- `ads_audience_reports_by_province_daily` +- `ads_audience_reports_daily` +- `ads_reports_daily` +- `ads_reports_hourly` +- `advertisers_audience_reports_by_country_daily` +- `advertisers_audience_reports_by_platform_daily` +- `advertisers_audience_reports_daily` +- `advertisers_reports_daily` +- `advertisers_reports_hourly` +- `campaigns` +- `campaigns_audience_reports_by_country_daily` +- `campaigns_audience_reports_by_platform_daily` +- `campaigns_audience_reports_daily` +- `campaigns_reports_daily` +- `campaigns_reports_hourly` +- `creative_assets_images` +- `creative_assets_videos` + +See `Clearing data` to update your connection. + +Schema changes for `advertiser_ids` stream. +Type of advertiser_id field was changed from integer to string to use actual data types as it's declared in API docs. Users will need to refresh stream schema. + +See `Refresh schemas` to update your connection. + +## Migration Steps + +### Refresh schemas + +Refreshing `advertiser_ids` schema is required in order to continue syncing `advertiser_ids` stream data. To refresh schema follow the steps below: + +1. Select **Connections** in the main navbar. + 1. Select the connection(s) affected by the update. +2. Select the **Schema** tab. + 1. Select **Refresh source schema**. + 2. Select **OK** and **Save changes**. +3. Select **Connections** in the main nav bar. + 1. Select the connection(s) affected by the update. +4. Select the **Status** tab. + 1. In the **Enabled streams** list, click the three dots on the right side of the `advertiser_ids` stream and select **Clear Data**. + +Important: If you were using `advertiser_ids` without provided advertiser_id in the source configuration you should firstly refresh source schema for `advertiser_ids` stream and then clear data for affected streams from the list above. + +### Clearing data + +Clearing your data is required in order to continue syncing affected stream from list above successfully. To clear your data for the streams, follow the steps below: + +1. Select **Connections** in the main nav bar. + 1. Select the connection(s) affected by the update. +2. Select the **Status** tab. + 1. In the **Enabled streams** list, click the three dots on the right side of the impacted stream and select **Clear Data**. +3. Do the same steps from 1-2.1 for all streams in your connection that were affected by this update. + +After the clear succeeds, trigger a sync by clicking **Sync Now**. For more information on clearing your data in Airbyte, see [this page](https://docs.airbyte.com/operator-guides/reset). + + diff --git a/docs/integrations/sources/tiktok-marketing.md b/docs/integrations/sources/tiktok-marketing.md index 712ff2431bea..258acf9666e0 100644 --- a/docs/integrations/sources/tiktok-marketing.md +++ b/docs/integrations/sources/tiktok-marketing.md @@ -66,7 +66,7 @@ To access the Sandbox environment: ## Supported streams and sync modes | Stream | Environment | Key | Incremental | -| :---------------------------------------- | :----------- | :----------------------------------------- | :---------- | +| :---------------------------------------- | :----------- | :----------------------------------------- |:------------| | Advertisers | Prod,Sandbox | advertiser_id | No | | AdGroups | Prod,Sandbox | adgroup_id | Yes | | Ads | Prod,Sandbox | ad_id | Yes | @@ -85,10 +85,10 @@ To access the Sandbox environment: | CampaignsReportsDaily | Prod,Sandbox | campaign_id, stat_time_day | Yes | | CampaignsReportsLifetime | Prod,Sandbox | campaign_id | No | | CreativeAssetsImages | Prod,Sandbox | image_id | Yes | -| CreativeAssetsMusic | Prod,Sandbox | music_id | Yes | +| CreativeAssetsMusic | Prod,Sandbox | music_id | No | | CreativeAssetsPortfolios | Prod,Sandbox | creative_portfolio_id | No | | CreativeAssetsVideos | Prod,Sandbox | video_id | Yes | -| AdvertiserIds | Prod | advertiser_id | Yes | +| AdvertiserIds | Prod | advertiser_id | No | | AdvertisersAudienceReportsDaily | Prod | advertiser_id, stat_time_day, gender, age | Yes | | AdvertisersAudienceReportsByCountryDaily | Prod | advertiser_id, stat_time_day, country_code | Yes | | AdvertisersAudienceReportsByPlatformDaily | Prod | advertiser_id, stat_time_day, platform | Yes | @@ -125,58 +125,59 @@ The connector is restricted by [requests limitation](https://business-api.tiktok Expand to review | Version | Date | Pull Request | Subject | -| :------ | :--------- | :------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------- | -| 3.9.10 | 2024-06-25 | [40373](https://github.com/airbytehq/airbyte/pull/40373) | Update dependencies | -| 3.9.9 | 2024-06-22 | [40133](https://github.com/airbytehq/airbyte/pull/40133) | Update dependencies | -| 3.9.8 | 2024-06-06 | [39253](https://github.com/airbytehq/airbyte/pull/39253) | [autopull] Upgrade base image to v1.2.2 | -| 3.9.7 | 2024-05-15 | [38250](https://github.com/airbytehq/airbyte/pull/38250) | Replace AirbyteLogger with logging.Logger and upgrade to latest base image | -| 3.9.6 | 2024-04-19 | [36665](https://github.com/airbytehq/airbyte/pull/36665) | Updating to 0.80.0 CDK | -| 3.9.5 | 2024-04-12 | [36665](https://github.com/airbytehq/airbyte/pull/36665) | Schema descriptions | -| 3.9.4 | 2024-03-20 | [36302](https://github.com/airbytehq/airbyte/pull/36302) | Don't extract state from the latest record if stream doesn't have a cursor_field | -| 3.9.3 | 2024-02-12 | [35161](https://github.com/airbytehq/airbyte/pull/35161) | Manage dependencies with Poetry. | -| 3.9.2 | 2023-11-02 | [32091](https://github.com/airbytehq/airbyte/pull/32091) | Fix incremental syncs; update docs; fix field type of `preview_url_expire_time` to `date-time`. | -| 3.9.1 | 2023-10-25 | [31812](https://github.com/airbytehq/airbyte/pull/31812) | Update `support level` in `metadata`, removed duplicated `tracking_pixel_id` field from `Ads` stream schema | -| 3.9.0 | 2023-10-23 | [31623](https://github.com/airbytehq/airbyte/pull/31623) | Add AdsAudienceReportsByProvince stream and expand base report metrics | -| 3.8.0 | 2023-10-19 | [31610](https://github.com/airbytehq/airbyte/pull/31610) | Add Creative Assets and Audiences streams | -| 3.7.1 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | -| 3.7.0 | 2023-10-19 | [31493](https://github.com/airbytehq/airbyte/pull/31493) | Add fields to Ads stream | -| 3.6.0 | 2023-10-18 | [31537](https://github.com/airbytehq/airbyte/pull/31537) | Use default availability strategy | -| 3.5.0 | 2023-10-16 | [31445](https://github.com/airbytehq/airbyte/pull/31445) | Apply minimum date restrictions | -| 3.4.1 | 2023-08-04 | [29083](https://github.com/airbytehq/airbyte/pull/29083) | Added new `is_smart_performance_campaign` property to `ad groups` stream schema | -| 3.4.0 | 2023-07-13 | [27910](https://github.com/airbytehq/airbyte/pull/27910) | Added `include_deleted` config param - include deleted `ad_groups`, `ad`, `campaigns` to reports | -| 3.3.1 | 2023-07-06 | [25423](https://github.com/airbytehq/airbyte/pull/25423) | Add new fields to ad reports streams | -| 3.3.0 | 2023-07-05 | [27988](https://github.com/airbytehq/airbyte/pull/27988) | Add `category_exclusion_ids` field to `ad_groups` schema. | -| 3.2.1 | 2023-05-26 | [26569](https://github.com/airbytehq/airbyte/pull/26569) | Fixed syncs with `advertiser_id` provided in input configuration | -| 3.2.0 | 2023-05-25 | [26565](https://github.com/airbytehq/airbyte/pull/26565) | Change default value for `attribution window` to 3 days; add min/max validation | -| 3.1.0 | 2023-05-12 | [26024](https://github.com/airbytehq/airbyte/pull/26024) | Updated the `Ads` stream schema | -| 3.0.1 | 2023-04-07 | [24712](https://github.com/airbytehq/airbyte/pull/24712) | Added `attribution window` for \*-reports streams | -| 3.0.0 | 2023-03-29 | [24630](https://github.com/airbytehq/airbyte/pull/24630) | Migrate to v1.3 API | -| 2.0.6 | 2023-03-30 | [22134](https://github.com/airbytehq/airbyte/pull/22134) | Add `country_code` and `platform` audience reports. | -| 2.0.5 | 2023-03-29 | [22863](https://github.com/airbytehq/airbyte/pull/22863) | Specified date formatting in specification | -| 2.0.4 | 2023-02-23 | [22309](https://github.com/airbytehq/airbyte/pull/22309) | Add Advertiser ID to filter reports and streams | -| 2.0.3 | 2023-02-15 | [23091](https://github.com/airbytehq/airbyte/pull/23091) | Add more clear log message for 504 error | -| 2.0.2 | 2023-02-02 | [22309](https://github.com/airbytehq/airbyte/pull/22309) | Chunk Advertiser IDs | -| 2.0.1 | 2023-01-27 | [22044](https://github.com/airbytehq/airbyte/pull/22044) | Set `AvailabilityStrategy` for streams explicitly to `None` | -| 2.0.0 | 2022-12-20 | [20415](https://github.com/airbytehq/airbyte/pull/20415) | Update schema types for `AudienceReports` and `BasicReports` streams. | -| 1.0.1 | 2022-12-16 | [20598](https://github.com/airbytehq/airbyte/pull/20598) | Remove Audience Reports with Hourly granularity due to deprecated dimension. | -| 1.0.0 | 2022-12-05 | [19758](https://github.com/airbytehq/airbyte/pull/19758) | Convert `mobile_app_id` from integer to string in AudienceReport streams. | -| 0.1.17 | 2022-10-04 | [17557](https://github.com/airbytehq/airbyte/pull/17557) | Retry error 50002 | -| 0.1.16 | 2022-09-28 | [17326](https://github.com/airbytehq/airbyte/pull/17326) | Migrate to per-stream state | -| 0.1.15 | 2022-08-30 | [16137](https://github.com/airbytehq/airbyte/pull/16137) | Fixed bug with normalization caused by unsupported nested cursor field | -| 0.1.14 | 2022-06-29 | [13890](https://github.com/airbytehq/airbyte/pull/13890) | Removed granularity config option | -| 0.1.13 | 2022-06-28 | [13650](https://github.com/airbytehq/airbyte/pull/13650) | Added video metrics to report streams | -| 0.1.12 | 2022-05-24 | [13127](https://github.com/airbytehq/airbyte/pull/13127) | Fixed integration test | -| 0.1.11 | 2022-04-27 | [12838](https://github.com/airbytehq/airbyte/pull/12838) | Added end date configuration for tiktok | -| 0.1.10 | 2022-05-07 | [12545](https://github.com/airbytehq/airbyte/pull/12545) | Removed odd production authenication method | -| 0.1.9 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy | -| 0.1.8 | 2022-04-28 | [12435](https://github.com/airbytehq/airbyte/pull/12435) | Updated spec descriptions | -| 0.1.7 | 2022-04-27 | [12380](https://github.com/airbytehq/airbyte/pull/12380) | Fixed spec descriptions and documentation | -| 0.1.6 | 2022-04-19 | [11378](https://github.com/airbytehq/airbyte/pull/11378) | Updated logic for stream initializations, fixed errors in schemas, updated SAT and unit tests | -| 0.1.5 | 2022-02-17 | [10398](https://github.com/airbytehq/airbyte/pull/10398) | Add Audience reports | -| 0.1.4 | 2021-12-30 | [7636](https://github.com/airbytehq/airbyte/pull/7636) | Add OAuth support | -| 0.1.3 | 2021-12-10 | [8425](https://github.com/airbytehq/airbyte/pull/8425) | Update title, description fields in spec | -| 0.1.2 | 2021-12-02 | [8292](https://github.com/airbytehq/airbyte/pull/8292) | Support reports | -| 0.1.1 | 2021-11-08 | [7499](https://github.com/airbytehq/airbyte/pull/7499) | Remove base-python dependencies | -| 0.1.0 | 2021-09-18 | [5887](https://github.com/airbytehq/airbyte/pull/5887) | Release TikTok Marketing CDK Connector | +|:--------|:-----------|:---------------------------------------------------------|:------------------------------------------------------------------------------------------------------------| +| 4.0.0 | 2024-07-01 | [38316](https://github.com/airbytehq/airbyte/pull/38316) | Migration to low-code CDK; Support include deleted statuses for Ads, Ad Groups and Campaign streams. | +| 3.9.10 | 2024-06-25 | [40373](https://github.com/airbytehq/airbyte/pull/40373) | Update dependencies | +| 3.9.9 | 2024-06-22 | [40133](https://github.com/airbytehq/airbyte/pull/40133) | Update dependencies | +| 3.9.8 | 2024-06-06 | [39253](https://github.com/airbytehq/airbyte/pull/39253) | [autopull] Upgrade base image to v1.2.2 | +| 3.9.7 | 2024-05-15 | [38250](https://github.com/airbytehq/airbyte/pull/38250) | Replace AirbyteLogger with logging.Logger and upgrade to latest base image | +| 3.9.6 | 2024-04-19 | [36665](https://github.com/airbytehq/airbyte/pull/36665) | Updating to 0.80.0 CDK | +| 3.9.5 | 2024-04-12 | [36665](https://github.com/airbytehq/airbyte/pull/36665) | Schema descriptions | +| 3.9.4 | 2024-03-20 | [36302](https://github.com/airbytehq/airbyte/pull/36302) | Don't extract state from the latest record if stream doesn't have a cursor_field | +| 3.9.3 | 2024-02-12 | [35161](https://github.com/airbytehq/airbyte/pull/35161) | Manage dependencies with Poetry. | +| 3.9.2 | 2023-11-02 | [32091](https://github.com/airbytehq/airbyte/pull/32091) | Fix incremental syncs; update docs; fix field type of `preview_url_expire_time` to `date-time`. | +| 3.9.1 | 2023-10-25 | [31812](https://github.com/airbytehq/airbyte/pull/31812) | Update `support level` in `metadata`, removed duplicated `tracking_pixel_id` field from `Ads` stream schema | +| 3.9.0 | 2023-10-23 | [31623](https://github.com/airbytehq/airbyte/pull/31623) | Add AdsAudienceReportsByProvince stream and expand base report metrics | +| 3.8.0 | 2023-10-19 | [31610](https://github.com/airbytehq/airbyte/pull/31610) | Add Creative Assets and Audiences streams | +| 3.7.1 | 2023-10-19 | [31599](https://github.com/airbytehq/airbyte/pull/31599) | Base image migration: remove Dockerfile and use the python-connector-base image | +| 3.7.0 | 2023-10-19 | [31493](https://github.com/airbytehq/airbyte/pull/31493) | Add fields to Ads stream | +| 3.6.0 | 2023-10-18 | [31537](https://github.com/airbytehq/airbyte/pull/31537) | Use default availability strategy | +| 3.5.0 | 2023-10-16 | [31445](https://github.com/airbytehq/airbyte/pull/31445) | Apply minimum date restrictions | +| 3.4.1 | 2023-08-04 | [29083](https://github.com/airbytehq/airbyte/pull/29083) | Added new `is_smart_performance_campaign` property to `ad groups` stream schema | +| 3.4.0 | 2023-07-13 | [27910](https://github.com/airbytehq/airbyte/pull/27910) | Added `include_deleted` config param - include deleted `ad_groups`, `ad`, `campaigns` to reports | +| 3.3.1 | 2023-07-06 | [25423](https://github.com/airbytehq/airbyte/pull/25423) | Add new fields to ad reports streams | +| 3.3.0 | 2023-07-05 | [27988](https://github.com/airbytehq/airbyte/pull/27988) | Add `category_exclusion_ids` field to `ad_groups` schema. | +| 3.2.1 | 2023-05-26 | [26569](https://github.com/airbytehq/airbyte/pull/26569) | Fixed syncs with `advertiser_id` provided in input configuration | +| 3.2.0 | 2023-05-25 | [26565](https://github.com/airbytehq/airbyte/pull/26565) | Change default value for `attribution window` to 3 days; add min/max validation | +| 3.1.0 | 2023-05-12 | [26024](https://github.com/airbytehq/airbyte/pull/26024) | Updated the `Ads` stream schema | +| 3.0.1 | 2023-04-07 | [24712](https://github.com/airbytehq/airbyte/pull/24712) | Added `attribution window` for \*-reports streams | +| 3.0.0 | 2023-03-29 | [24630](https://github.com/airbytehq/airbyte/pull/24630) | Migrate to v1.3 API | +| 2.0.6 | 2023-03-30 | [22134](https://github.com/airbytehq/airbyte/pull/22134) | Add `country_code` and `platform` audience reports. | +| 2.0.5 | 2023-03-29 | [22863](https://github.com/airbytehq/airbyte/pull/22863) | Specified date formatting in specification | +| 2.0.4 | 2023-02-23 | [22309](https://github.com/airbytehq/airbyte/pull/22309) | Add Advertiser ID to filter reports and streams | +| 2.0.3 | 2023-02-15 | [23091](https://github.com/airbytehq/airbyte/pull/23091) | Add more clear log message for 504 error | +| 2.0.2 | 2023-02-02 | [22309](https://github.com/airbytehq/airbyte/pull/22309) | Chunk Advertiser IDs | +| 2.0.1 | 2023-01-27 | [22044](https://github.com/airbytehq/airbyte/pull/22044) | Set `AvailabilityStrategy` for streams explicitly to `None` | +| 2.0.0 | 2022-12-20 | [20415](https://github.com/airbytehq/airbyte/pull/20415) | Update schema types for `AudienceReports` and `BasicReports` streams. | +| 1.0.1 | 2022-12-16 | [20598](https://github.com/airbytehq/airbyte/pull/20598) | Remove Audience Reports with Hourly granularity due to deprecated dimension. | +| 1.0.0 | 2022-12-05 | [19758](https://github.com/airbytehq/airbyte/pull/19758) | Convert `mobile_app_id` from integer to string in AudienceReport streams. | +| 0.1.17 | 2022-10-04 | [17557](https://github.com/airbytehq/airbyte/pull/17557) | Retry error 50002 | +| 0.1.16 | 2022-09-28 | [17326](https://github.com/airbytehq/airbyte/pull/17326) | Migrate to per-stream state | +| 0.1.15 | 2022-08-30 | [16137](https://github.com/airbytehq/airbyte/pull/16137) | Fixed bug with normalization caused by unsupported nested cursor field | +| 0.1.14 | 2022-06-29 | [13890](https://github.com/airbytehq/airbyte/pull/13890) | Removed granularity config option | +| 0.1.13 | 2022-06-28 | [13650](https://github.com/airbytehq/airbyte/pull/13650) | Added video metrics to report streams | +| 0.1.12 | 2022-05-24 | [13127](https://github.com/airbytehq/airbyte/pull/13127) | Fixed integration test | +| 0.1.11 | 2022-04-27 | [12838](https://github.com/airbytehq/airbyte/pull/12838) | Added end date configuration for tiktok | +| 0.1.10 | 2022-05-07 | [12545](https://github.com/airbytehq/airbyte/pull/12545) | Removed odd production authenication method | +| 0.1.9 | 2022-04-30 | [12500](https://github.com/airbytehq/airbyte/pull/12500) | Improve input configuration copy | +| 0.1.8 | 2022-04-28 | [12435](https://github.com/airbytehq/airbyte/pull/12435) | Updated spec descriptions | +| 0.1.7 | 2022-04-27 | [12380](https://github.com/airbytehq/airbyte/pull/12380) | Fixed spec descriptions and documentation | +| 0.1.6 | 2022-04-19 | [11378](https://github.com/airbytehq/airbyte/pull/11378) | Updated logic for stream initializations, fixed errors in schemas, updated SAT and unit tests | +| 0.1.5 | 2022-02-17 | [10398](https://github.com/airbytehq/airbyte/pull/10398) | Add Audience reports | +| 0.1.4 | 2021-12-30 | [7636](https://github.com/airbytehq/airbyte/pull/7636) | Add OAuth support | +| 0.1.3 | 2021-12-10 | [8425](https://github.com/airbytehq/airbyte/pull/8425) | Update title, description fields in spec | +| 0.1.2 | 2021-12-02 | [8292](https://github.com/airbytehq/airbyte/pull/8292) | Support reports | +| 0.1.1 | 2021-11-08 | [7499](https://github.com/airbytehq/airbyte/pull/7499) | Remove base-python dependencies | +| 0.1.0 | 2021-09-18 | [5887](https://github.com/airbytehq/airbyte/pull/5887) | Release TikTok Marketing CDK Connector |