1
0
mirror of synced 2026-01-08 21:05:13 -05:00
Files
airbyte/airbyte-integrations/connectors/source-amazon-ads/source_amazon_ads/source.py
midavadim d1802154ae 🎉 Amazon Ads - new streams for bids and keyword recommendations (#28002)
* added new streams for amazon adgroup bids keyword recommendations

* updated docs and configured catalog

* updated version in metadata

* fix test

* updated docs

* fix doc after merge

* fix comments

* updated version

* fux typo
2023-07-06 13:18:33 -04:00

146 lines
6.0 KiB
Python

#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
import logging
from typing import Any, List, Mapping, Optional, Tuple
import pendulum
from airbyte_cdk.sources import AbstractSource
from airbyte_cdk.sources.streams import Stream
from airbyte_cdk.sources.streams.http.auth import Oauth2Authenticator
from .schemas import Profile
from .streams import (
AttributionReportPerformanceAdgroup,
AttributionReportPerformanceCampaign,
AttributionReportPerformanceCreative,
AttributionReportProducts,
Portfolios,
Profiles,
SponsoredBrandsAdGroups,
SponsoredBrandsCampaigns,
SponsoredBrandsKeywords,
SponsoredBrandsReportStream,
SponsoredBrandsV3ReportStream,
SponsoredBrandsVideoReportStream,
SponsoredDisplayAdGroups,
SponsoredDisplayBudgetRules,
SponsoredDisplayCampaigns,
SponsoredDisplayProductAds,
SponsoredDisplayReportStream,
SponsoredDisplayTargetings,
SponsoredProductAdGroupBidRecommendations,
SponsoredProductAdGroups,
SponsoredProductAdGroupSuggestedKeywords,
SponsoredProductAds,
SponsoredProductCampaignNegativeKeywords,
SponsoredProductCampaigns,
SponsoredProductKeywords,
SponsoredProductNegativeKeywords,
SponsoredProductsReportStream,
SponsoredProductTargetings,
)
# Oauth 2.0 authentication URL for amazon
TOKEN_URL = "https://api.amazon.com/auth/o2/token"
CONFIG_DATE_FORMAT = "YYYY-MM-DD"
class SourceAmazonAds(AbstractSource):
def _validate_and_transform(self, config: Mapping[str, Any]):
start_date = config.get("start_date")
if start_date:
config["start_date"] = pendulum.from_format(start_date, CONFIG_DATE_FORMAT).date()
else:
config["start_date"] = None
if not config.get("region"):
source_spec = self.spec(logging.getLogger("airbyte"))
config["region"] = source_spec.connectionSpecification["properties"]["region"]["default"]
if not config.get("look_back_window"):
source_spec = self.spec(logging.getLogger("airbyte"))
config["look_back_window"] = source_spec.connectionSpecification["properties"]["look_back_window"]["default"]
config["report_record_types"] = config.get("report_record_types", [])
return config
def check_connection(self, logger: logging.Logger, config: Mapping[str, Any]) -> Tuple[bool, Optional[Any]]:
"""
:param config: the user-input config object conforming to the connector's spec.json
:param logger: logger object
:return Tuple[bool, any]: (True, None) if the input config can be used to connect to the API successfully, (False, error) otherwise.
"""
try:
config = self._validate_and_transform(config)
except Exception as e:
return False, str(e)
# Check connection by sending list of profiles request. Its most simple
# request, not require additional parameters and usually has few data
# in response body.
# It doesnt support pagination so there is no sense of reading single
# record, it would fetch all the data anyway.
Profiles(config, authenticator=self._make_authenticator(config)).get_all_profiles()
return True, None
def streams(self, config: Mapping[str, Any]) -> List[Stream]:
"""
:param config: A Mapping of the user input configuration as defined in the connector spec.
:return list of streams for current source
"""
config = self._validate_and_transform(config)
auth = self._make_authenticator(config)
stream_args = {"config": config, "authenticator": auth}
# All data for individual Amazon Ads stream divided into sets of data for
# each profile. Every API request except profiles has required
# paramater passed over "Amazon-Advertising-API-Scope" http header and
# should contain profile id. So every stream is dependant on Profiles
# stream and should have information about all profiles.
profiles_stream = Profiles(**stream_args)
profiles_list = profiles_stream.get_all_profiles()
stream_args["profiles"] = self._choose_profiles(config, profiles_list)
non_profile_stream_classes = [
SponsoredDisplayCampaigns,
SponsoredDisplayAdGroups,
SponsoredDisplayProductAds,
SponsoredDisplayTargetings,
SponsoredDisplayReportStream,
SponsoredDisplayBudgetRules,
SponsoredProductCampaigns,
SponsoredProductAdGroups,
SponsoredProductAdGroupBidRecommendations,
SponsoredProductAdGroupSuggestedKeywords,
SponsoredProductKeywords,
SponsoredProductNegativeKeywords,
SponsoredProductCampaignNegativeKeywords,
SponsoredProductAds,
SponsoredProductTargetings,
SponsoredProductsReportStream,
SponsoredBrandsCampaigns,
SponsoredBrandsAdGroups,
SponsoredBrandsKeywords,
SponsoredBrandsReportStream,
SponsoredBrandsV3ReportStream,
SponsoredBrandsVideoReportStream,
AttributionReportPerformanceAdgroup,
AttributionReportPerformanceCampaign,
AttributionReportPerformanceCreative,
AttributionReportProducts,
]
portfolios_stream = Portfolios(**stream_args)
return [profiles_stream, portfolios_stream, *[stream_class(**stream_args) for stream_class in non_profile_stream_classes]]
@staticmethod
def _make_authenticator(config: Mapping[str, Any]):
return Oauth2Authenticator(
token_refresh_endpoint=TOKEN_URL,
client_id=config["client_id"],
client_secret=config["client_secret"],
refresh_token=config["refresh_token"],
)
@staticmethod
def _choose_profiles(config: Mapping[str, Any], profiles: List[Profile]):
if not config.get("profiles"):
return profiles
return list(filter(lambda profile: profile.profileId in config["profiles"], profiles))